diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index d7f271535228c2..0d162c068376fa 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -34,6 +34,14 @@ export const DEFAULT_INTERVAL_VALUE = 300000; // ms export const DEFAULT_TIMEPICKER_QUICK_RANGES = 'timepicker:quickRanges'; export const NO_ALERT_INDEX = 'no-alert-index-049FC71A-4C2C-446F-9901-37XMC5024C51'; +export const APP_OVERVIEW_PATH = `${APP_PATH}/overview`; +export const APP_ALERTS_PATH = `${APP_PATH}/alerts`; +export const APP_HOSTS_PATH = `${APP_PATH}/hosts`; +export const APP_NETWORK_PATH = `${APP_PATH}/network`; +export const APP_TIMELINES_PATH = `${APP_PATH}/timelines`; +export const APP_CASES_PATH = `${APP_PATH}/cases`; +export const APP_MANAGEMENT_PATH = `${APP_PATH}/management`; + /** The comma-delimited list of Elasticsearch indices from which the SIEM app collects events */ export const DEFAULT_INDEX_PATTERN = [ 'apm-*-transaction*', diff --git a/x-pack/plugins/security_solution/cypress/integration/detections.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detections.spec.ts index 43acdd40dadcc9..2e727be1fc9b45 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detections.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detections.spec.ts @@ -28,13 +28,13 @@ import { import { esArchiverLoad } from '../tasks/es_archiver'; import { loginAndWaitForPage } from '../tasks/login'; -import { DETECTIONS } from '../urls/navigation'; +import { ALERTS_URL } from '../urls/navigation'; describe('Detections', () => { context('Closing alerts', () => { beforeEach(() => { esArchiverLoad('alerts'); - loginAndWaitForPage(DETECTIONS); + loginAndWaitForPage(ALERTS_URL); }); it('Closes and opens alerts', () => { @@ -161,7 +161,7 @@ describe('Detections', () => { context('Opening alerts', () => { beforeEach(() => { esArchiverLoad('closed_alerts'); - loginAndWaitForPage(DETECTIONS); + loginAndWaitForPage(ALERTS_URL); }); it('Open one alert when more than one closed alerts are selected', () => { @@ -207,7 +207,7 @@ describe('Detections', () => { context('Marking alerts as in-progress', () => { beforeEach(() => { esArchiverLoad('alerts'); - loginAndWaitForPage(DETECTIONS); + loginAndWaitForPage(ALERTS_URL); }); it('Mark one alert in progress when more than one open alerts are selected', () => { diff --git a/x-pack/plugins/security_solution/cypress/integration/detections_timeline.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detections_timeline.spec.ts index d3ddb2ad71e30a..91617981ab14c4 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detections_timeline.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detections_timeline.spec.ts @@ -15,12 +15,12 @@ import { import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; import { loginAndWaitForPage } from '../tasks/login'; -import { DETECTIONS } from '../urls/navigation'; +import { ALERTS_URL } from '../urls/navigation'; describe('Detections timeline', () => { beforeEach(() => { esArchiverLoad('timeline_alerts'); - loginAndWaitForPage(DETECTIONS); + loginAndWaitForPage(ALERTS_URL); }); afterEach(() => { diff --git a/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts b/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts index 785df4a0c2f9e6..0c3424576e4cf8 100644 --- a/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/ml_conditional_links.spec.ts @@ -101,7 +101,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkSingleIpNullKqlQuery); cy.url().should( 'include', - '/app/security#/network/ip/127.0.0.1/source?timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))' + '/app/security/network/ip/127.0.0.1/source?timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))' ); }); @@ -109,7 +109,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkSingleIpKqlQuery); cy.url().should( 'include', - "/app/security#/network/ip/127.0.0.1/source?query=(language:kuery,query:'(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)')&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))" + '/app/security/network/ip/127.0.0.1/source?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))' ); }); @@ -117,7 +117,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkMultipleIpNullKqlQuery); cy.url().should( 'include', - "app/security#/network/flows?query=(language:kuery,query:'((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))')&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999))" + 'app/security/network/flows?query=(language:kuery,query:%27((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999))' ); }); @@ -125,7 +125,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkMultipleIpKqlQuery); cy.url().should( 'include', - "/app/security#/network/flows?query=(language:kuery,query:'((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))')&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))" + '/app/security/network/flows?query=(language:kuery,query:%27((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))' ); }); @@ -133,7 +133,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkNullKqlQuery); cy.url().should( 'include', - '/app/security#/network/flows?timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))' + '/app/security/network/flows?timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))' ); }); @@ -141,7 +141,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkKqlQuery); cy.url().should( 'include', - "/app/security#/network/flows?query=(language:kuery,query:'(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)')&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))" + '/app/security/network/flows?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))' ); }); @@ -149,7 +149,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostSingleHostNullKqlQuery); cy.url().should( 'include', - '/app/security#/hosts/siem-windows/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))' + '/app/security/hosts/siem-windows/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))' ); }); @@ -157,7 +157,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostSingleHostKqlQueryVariable); cy.url().should( 'include', - '/app/security#/hosts/siem-windows/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))' + '/app/security/hosts/siem-windows/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))' ); }); @@ -165,7 +165,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostSingleHostKqlQuery); cy.url().should( 'include', - "/app/security#/hosts/siem-windows/anomalies?query=(language:kuery,query:'(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)')&timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))" + '/app/security/hosts/siem-windows/anomalies?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))' ); }); @@ -173,7 +173,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostMultiHostNullKqlQuery); cy.url().should( 'include', - "/app/security#/hosts/anomalies?query=(language:kuery,query:'(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)')&timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))" + '/app/security/hosts/anomalies?query=(language:kuery,query:%27(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))' ); }); @@ -181,7 +181,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostMultiHostKqlQuery); cy.url().should( 'include', - "/app/security#/hosts/anomalies?query=(language:kuery,query:'(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))')&timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))" + '/app/security/hosts/anomalies?query=(language:kuery,query:%27(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))' ); }); @@ -189,7 +189,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostVariableHostNullKqlQuery); cy.url().should( 'include', - '/app/security#/hosts/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))' + '/app/security/hosts/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))' ); }); @@ -197,7 +197,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostVariableHostKqlQuery); cy.url().should( 'include', - "/app/security#/hosts/anomalies?query=(language:kuery,query:'(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)')&timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))" + '/app/security/hosts/anomalies?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:1559800800000,kind:absolute,to:1559887199999)),timeline:(linkTo:!(global),timerange:(from:1559800800000,kind:absolute,to:1559887199999)))' ); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/navigation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/navigation.spec.ts index 2014e34c118864..67b72982f44e04 100644 --- a/x-pack/plugins/security_solution/cypress/integration/navigation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/navigation.spec.ts @@ -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 { DETECTIONS, HOSTS, NETWORK, OVERVIEW, TIMELINES } from '../screens/security_header'; +import { ALERTS, HOSTS, NETWORK, OVERVIEW, TIMELINES } from '../screens/security_header'; import { loginAndWaitForPage } from '../tasks/login'; import { navigateFromHeaderTo } from '../tasks/security_header'; @@ -16,26 +16,26 @@ describe('top-level navigation common to all pages in the Security app', () => { }); it('navigates to the Overview page', () => { navigateFromHeaderTo(OVERVIEW); - cy.url().should('include', '/security#/overview'); + cy.url().should('include', '/security/overview'); }); it('navigates to the Hosts page', () => { navigateFromHeaderTo(HOSTS); - cy.url().should('include', '/security#/hosts'); + cy.url().should('include', '/security/hosts'); }); it('navigates to the Network page', () => { navigateFromHeaderTo(NETWORK); - cy.url().should('include', '/security#/network'); + cy.url().should('include', '/security/network'); }); - it('navigates to the Detections page', () => { - navigateFromHeaderTo(DETECTIONS); - cy.url().should('include', '/security#/detections'); + it('navigates to the Alerts page', () => { + navigateFromHeaderTo(ALERTS); + cy.url().should('include', '/security/alerts'); }); it('navigates to the Timelines page', () => { navigateFromHeaderTo(TIMELINES); - cy.url().should('include', '/security#/timelines'); + cy.url().should('include', '/security/timelines'); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/signal_detection_rules.spec.ts b/x-pack/plugins/security_solution/cypress/integration/signal_detection_rules.spec.ts index e8f9411c149d46..72a86e3ffffc56 100644 --- a/x-pack/plugins/security_solution/cypress/integration/signal_detection_rules.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/signal_detection_rules.spec.ts @@ -26,7 +26,7 @@ import { waitForRuleToBeActivated, } from '../tasks/alert_detection_rules'; -import { DETECTIONS } from '../urls/navigation'; +import { ALERTS_URL } from '../urls/navigation'; describe('Detection rules', () => { before(() => { @@ -38,7 +38,7 @@ describe('Detection rules', () => { }); it('Sorts by activated rules', () => { - loginAndWaitForPageWithoutDateRange(DETECTIONS); + loginAndWaitForPageWithoutDateRange(ALERTS_URL); waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertDetectionRules(); diff --git a/x-pack/plugins/security_solution/cypress/integration/signal_detection_rules_custom.spec.ts b/x-pack/plugins/security_solution/cypress/integration/signal_detection_rules_custom.spec.ts index e5cec16c48a379..48d0c2e7238cd4 100644 --- a/x-pack/plugins/security_solution/cypress/integration/signal_detection_rules_custom.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/signal_detection_rules_custom.spec.ts @@ -62,7 +62,7 @@ import { import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; -import { DETECTIONS } from '../urls/navigation'; +import { ALERTS_URL } from '../urls/navigation'; describe('Detection rules, custom', () => { before(() => { @@ -74,7 +74,7 @@ describe('Detection rules, custom', () => { }); it('Creates and activates a new custom rule', () => { - loginAndWaitForPageWithoutDateRange(DETECTIONS); + loginAndWaitForPageWithoutDateRange(ALERTS_URL); waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertDetectionRules(); @@ -169,7 +169,7 @@ describe('Detection rules, custom', () => { describe('Deletes custom rules', () => { beforeEach(() => { esArchiverLoad('custom_rules'); - loginAndWaitForPageWithoutDateRange(DETECTIONS); + loginAndWaitForPageWithoutDateRange(ALERTS_URL); waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertDetectionRules(); diff --git a/x-pack/plugins/security_solution/cypress/integration/signal_detection_rules_export.spec.ts b/x-pack/plugins/security_solution/cypress/integration/signal_detection_rules_export.spec.ts index 4a12990438999e..edb559bf6a279e 100644 --- a/x-pack/plugins/security_solution/cypress/integration/signal_detection_rules_export.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/signal_detection_rules_export.spec.ts @@ -13,7 +13,7 @@ import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; import { exportFirstRule } from '../tasks/alert_detection_rules'; -import { DETECTIONS } from '../urls/navigation'; +import { ALERTS_URL } from '../urls/navigation'; const EXPECTED_EXPORTED_RULE_FILE_PATH = 'cypress/test_files/expected_rules_export.ndjson'; @@ -32,7 +32,7 @@ describe('Export rules', () => { }); it('Exports a custom rule', () => { - loginAndWaitForPageWithoutDateRange(DETECTIONS); + loginAndWaitForPageWithoutDateRange(ALERTS_URL); waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertDetectionRules(); diff --git a/x-pack/plugins/security_solution/cypress/integration/signal_detection_rules_ml.spec.ts b/x-pack/plugins/security_solution/cypress/integration/signal_detection_rules_ml.spec.ts index fd2dff27ad3590..3e0fc2e1b37fd0 100644 --- a/x-pack/plugins/security_solution/cypress/integration/signal_detection_rules_ml.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/signal_detection_rules_ml.spec.ts @@ -58,7 +58,7 @@ import { import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; -import { DETECTIONS } from '../urls/navigation'; +import { ALERTS_URL } from '../urls/navigation'; describe('Detection rules, machine learning', () => { before(() => { @@ -70,7 +70,7 @@ describe('Detection rules, machine learning', () => { }); it('Creates and activates a new ml rule', () => { - loginAndWaitForPageWithoutDateRange(DETECTIONS); + loginAndWaitForPageWithoutDateRange(ALERTS_URL); waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertDetectionRules(); diff --git a/x-pack/plugins/security_solution/cypress/integration/signal_detection_rules_prebuilt.spec.ts b/x-pack/plugins/security_solution/cypress/integration/signal_detection_rules_prebuilt.spec.ts index 2cd087b2ca5e1d..f819c91a773749 100644 --- a/x-pack/plugins/security_solution/cypress/integration/signal_detection_rules_prebuilt.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/signal_detection_rules_prebuilt.spec.ts @@ -31,7 +31,7 @@ import { import { esArchiverLoadEmptyKibana, esArchiverUnloadEmptyKibana } from '../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; -import { DETECTIONS } from '../urls/navigation'; +import { ALERTS_URL } from '../urls/navigation'; import { totalNumberOfPrebuiltRules } from '../objects/rule'; @@ -48,7 +48,7 @@ describe('Detection rules, prebuilt rules', () => { const expectedNumberOfRules = totalNumberOfPrebuiltRules; const expectedElasticRulesBtnText = `Elastic rules (${expectedNumberOfRules})`; - loginAndWaitForPageWithoutDateRange(DETECTIONS); + loginAndWaitForPageWithoutDateRange(ALERTS_URL); waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertDetectionRules(); @@ -73,7 +73,7 @@ describe('Deleting prebuilt rules', () => { const expectedElasticRulesBtnText = `Elastic rules (${expectedNumberOfRules})`; esArchiverLoadEmptyKibana(); - loginAndWaitForPageWithoutDateRange(DETECTIONS); + loginAndWaitForPageWithoutDateRange(ALERTS_URL); waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); goToManageAlertDetectionRules(); diff --git a/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts b/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts new file mode 100644 index 00000000000000..911fd7e0f34832 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts @@ -0,0 +1,17 @@ +/* + * 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 { loginAndWaitForPage } from '../tasks/login'; + +import { DETECTIONS } from '../urls/navigation'; + +describe('URL compatibility', () => { + it('Redirects to Alerts from old Detections URL', () => { + loginAndWaitForPage(DETECTIONS); + + cy.url().should('include', '/security/alerts'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts index 425ed23bdafec0..1cefa7fe73d356 100644 --- a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts @@ -166,7 +166,10 @@ describe('url state', () => { loginAndWaitForPageWithoutDateRange(ABSOLUTE_DATE_RANGE.url); kqlSearch('source.ip: "10.142.0.9" {enter}'); - cy.url().should('include', `query=(language:kuery,query:'source.ip:%20%2210.142.0.9%22%20')`); + cy.url().should( + 'include', + `query=(language:kuery,query:%27source.ip:%20%2210.142.0.9%22%20%27)` + ); }); it('sets the url state when kql is set and check if href reflect this change', () => { @@ -177,7 +180,7 @@ describe('url state', () => { cy.get(NETWORK).should( 'have.attr', 'href', - "#/link-to/network?query=(language:kuery,query:'source.ip:%20%2210.142.0.9%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))" + `/app/security/network?query=(language:kuery,query:'source.ip:%20%2210.142.0.9%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))` ); }); @@ -190,12 +193,12 @@ describe('url state', () => { cy.get(HOSTS).should( 'have.attr', 'href', - "#/link-to/hosts?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))" + `/app/security/hosts?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))` ); cy.get(NETWORK).should( 'have.attr', 'href', - "#/link-to/network?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))" + `/app/security/network?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))` ); cy.get(HOSTS_NAMES).first().invoke('text').should('eq', 'siem-kibana'); @@ -206,21 +209,21 @@ describe('url state', () => { cy.get(ANOMALIES_TAB).should( 'have.attr', 'href', - "#/hosts/siem-kibana/anomalies?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))" + "/app/security/hosts/siem-kibana/anomalies?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))" ); cy.get(BREADCRUMBS) .eq(1) .should( 'have.attr', 'href', - "#/link-to/hosts?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))" + `/app/security/hosts?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))` ); cy.get(BREADCRUMBS) .eq(2) .should( 'have.attr', 'href', - "#/link-to/hosts/siem-kibana?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))" + `/app/security/hosts/siem-kibana?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))` ); }); @@ -231,7 +234,7 @@ describe('url state', () => { cy.get(KQL_INPUT).should('have.attr', 'value', 'source.ip: "10.142.0.9"'); }); - it('sets and reads the url state for timeline by id', () => { + it.skip('sets and reads the url state for timeline by id', () => { loginAndWaitForPage(HOSTS_PAGE); openTimeline(); executeTimelineKQL('host.name: *'); @@ -254,10 +257,11 @@ describe('url state', () => { const newTimelineId = matched && matched.length > 0 ? matched[0] : 'null'; expect(matched).to.have.lengthOf(1); closeTimeline(); - cy.visit('/app/kibana'); - cy.visit(`/app/security#/overview?timeline\=(id:'${newTimelineId}',isOpen:!t)`); + cy.visit('/app/home'); + cy.visit(`/app/security/timelines?timeline=(id:'${newTimelineId}',isOpen:!t)`); cy.contains('a', 'Security'); cy.get(DATE_PICKER_APPLY_BUTTON_TIMELINE).invoke('text').should('not.equal', 'Updating'); + cy.get(TIMELINE_TITLE).should('be.visible'); cy.get(TIMELINE_TITLE).should('have.attr', 'value', timelineName); }); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/security_header.ts b/x-pack/plugins/security_solution/cypress/screens/security_header.ts index c2dab051793c1a..89deeee0426d07 100644 --- a/x-pack/plugins/security_solution/cypress/screens/security_header.ts +++ b/x-pack/plugins/security_solution/cypress/screens/security_header.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -export const BREADCRUMBS = '[data-test-subj="breadcrumbs"] a'; +export const ALERTS = '[data-test-subj="navigation-alerts"]'; -export const DETECTIONS = '[data-test-subj="navigation-detections"]'; +export const BREADCRUMBS = '[data-test-subj="breadcrumbs"] a'; export const HOSTS = '[data-test-subj="navigation-hosts"]'; diff --git a/x-pack/plugins/security_solution/cypress/urls/ml_conditional_links.ts b/x-pack/plugins/security_solution/cypress/urls/ml_conditional_links.ts index cfa18099e58882..655418fc98bf8e 100644 --- a/x-pack/plugins/security_solution/cypress/urls/ml_conditional_links.ts +++ b/x-pack/plugins/security_solution/cypress/urls/ml_conditional_links.ts @@ -25,52 +25,52 @@ // Single IP with a null for the Query: export const mlNetworkSingleIpNullKqlQuery = - "/app/security#/ml-network/ip/127.0.0.1?query=!n&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')))"; + "/app/siem#/ml-network/ip/127.0.0.1?query=!n&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')))"; // Single IP with a value for the Query: export const mlNetworkSingleIpKqlQuery = - "/app/security#/ml-network/ip/127.0.0.1?query=(language:kuery,query:'process.name%20:%20%22conhost.exe,sc.exe%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')))"; + "/app/siem#/ml-network/ip/127.0.0.1?query=(language:kuery,query:'process.name%20:%20%22conhost.exe,sc.exe%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')))"; // Multiple IPs with a null for the Query: export const mlNetworkMultipleIpNullKqlQuery = - "/app/security#/ml-network/ip/127.0.0.1,127.0.0.2?query=!n&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')))"; + "/app/siem#/ml-network/ip/127.0.0.1,127.0.0.2?query=!n&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')))"; // Multiple IPs with a value for the Query: export const mlNetworkMultipleIpKqlQuery = - "/app/security#/ml-network/ip/127.0.0.1,127.0.0.2?query=(language:kuery,query:'process.name%20:%20%22conhost.exe,sc.exe%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')))"; + "/app/siem#/ml-network/ip/127.0.0.1,127.0.0.2?query=(language:kuery,query:'process.name%20:%20%22conhost.exe,sc.exe%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')))"; // $ip$ with a null Query: export const mlNetworkNullKqlQuery = - "/app/security#/ml-network/ip/$ip$?query=!n&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')))"; + "/app/siem#/ml-network/ip/$ip$?query=!n&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')))"; // $ip$ with a value for the Query: export const mlNetworkKqlQuery = - "/app/security#/ml-network/ip/$ip$?query=(language:kuery,query:'process.name%20:%20%22conhost.exe,sc.exe%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')))"; + "/app/siem#/ml-network/ip/$ip$?query=(language:kuery,query:'process.name%20:%20%22conhost.exe,sc.exe%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-28T11:00:00.000Z',kind:absolute,to:'2019-08-28T13:59:59.999Z')))"; // Single host name with a null for the Query: export const mlHostSingleHostNullKqlQuery = - "/app/security#/ml-hosts/siem-windows?query=!n&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; + "/app/siem#/ml-hosts/siem-windows?query=!n&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; // Single host name with a variable in the Query: export const mlHostSingleHostKqlQueryVariable = - "/app/security#/ml-hosts/siem-windows?query=(language:kuery,query:'process.name%20:%20%22$process.name$%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; + "/app/siem#/ml-hosts/siem-windows?query=(language:kuery,query:'process.name%20:%20%22$process.name$%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; // Single host name with a value for Query: export const mlHostSingleHostKqlQuery = - "/app/security#/ml-hosts/siem-windows?query=(language:kuery,query:'process.name%20:%20%22conhost.exe,sc.exe%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; + "/app/siem#/ml-hosts/siem-windows?query=(language:kuery,query:'process.name%20:%20%22conhost.exe,sc.exe%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; // Multiple host names with null for Query: export const mlHostMultiHostNullKqlQuery = - "/app/security#/ml-hosts/siem-windows,siem-suricata?query=!n&&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; + "/app/siem#/ml-hosts/siem-windows,siem-suricata?query=!n&&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; // Multiple host names with a value for Query: export const mlHostMultiHostKqlQuery = - "/app/security#/ml-hosts/siem-windows,siem-suricata?query=(language:kuery,query:'process.name%20:%20%22conhost.exe,sc.exe%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; + "/app/siem#/ml-hosts/siem-windows,siem-suricata?query=(language:kuery,query:'process.name%20:%20%22conhost.exe,sc.exe%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; // Undefined/null host name with a null for the KQL: export const mlHostVariableHostNullKqlQuery = - "/app/security#/ml-hosts/$host.name$?query=!n&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; + "/app/siem#/ml-hosts/$host.name$?query=!n&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; // Undefined/null host name but with a value for Query: export const mlHostVariableHostKqlQuery = - "/app/security#/ml-hosts/$host.name$?query=(language:kuery,query:'process.name%20:%20%22conhost.exe,sc.exe%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; + "/app/siem#/ml-hosts/$host.name$?query=(language:kuery,query:'process.name%20:%20%22conhost.exe,sc.exe%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-06-06T06:00:00.000Z',kind:absolute,to:'2019-06-07T05:59:59.999Z')))"; diff --git a/x-pack/plugins/security_solution/cypress/urls/navigation.ts b/x-pack/plugins/security_solution/cypress/urls/navigation.ts index 9bfe2e9e5102e0..7978aebfb413bd 100644 --- a/x-pack/plugins/security_solution/cypress/urls/navigation.ts +++ b/x-pack/plugins/security_solution/cypress/urls/navigation.ts @@ -4,16 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -export const CASES = '/app/security#/case'; -export const DETECTIONS = 'app/security#/detections'; -export const HOSTS_PAGE = '/app/security#/hosts/allHosts'; +export const ALERTS_URL = 'app/security/alerts'; +export const CASES = '/app/security/cases'; +export const DETECTIONS = '/app/siem#/detections'; +export const HOSTS_PAGE = '/app/security/hosts/allHosts'; export const HOSTS_PAGE_TAB_URLS = { - allHosts: '/app/security#/hosts/allHosts', - anomalies: '/app/security#/hosts/anomalies', - authentications: '/app/security#/hosts/authentications', - events: '/app/security#/hosts/events', - uncommonProcesses: '/app/security#/hosts/uncommonProcesses', + allHosts: '/app/security/hosts/allHosts', + anomalies: '/app/security/hosts/anomalies', + authentications: '/app/security/hosts/authentications', + events: '/app/security/hosts/events', + uncommonProcesses: '/app/security/hosts/uncommonProcesses', }; -export const NETWORK_PAGE = '/app/security#/network'; -export const OVERVIEW_PAGE = '/app/security#/overview'; -export const TIMELINES_PAGE = '/app/security#/timelines'; +export const NETWORK_PAGE = '/app/security/network'; +export const OVERVIEW_PAGE = '/app/security/overview'; +export const TIMELINES_PAGE = '/app/security/timelines'; diff --git a/x-pack/plugins/security_solution/cypress/urls/state.ts b/x-pack/plugins/security_solution/cypress/urls/state.ts index 6de30fdafdaf80..bdd90c21fbedf4 100644 --- a/x-pack/plugins/security_solution/cypress/urls/state.ts +++ b/x-pack/plugins/security_solution/cypress/urls/state.ts @@ -6,16 +6,16 @@ export const ABSOLUTE_DATE_RANGE = { url: - '/app/security#/network/?timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))', + '/app/security/network/flows/?timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))', urlUnlinked: - '/app/security#/network/?timerange=(global:(linkTo:!(),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(),timerange:(from:1564776209186,kind:absolute,to:1564779809186)))', - urlKqlNetworkNetwork: `/app/security#/network/?query=(language:kuery,query:'source.ip:%20"10.142.0.9"')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, - urlKqlNetworkHosts: `/app/security#/network/?query=(language:kuery,query:'source.ip:%20"10.142.0.9"')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, - urlKqlHostsNetwork: `/app/security#/hosts/allHosts?query=(language:kuery,query:'source.ip:%20"10.142.0.9"')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, - urlKqlHostsHosts: `/app/security#/hosts/allHosts?query=(language:kuery,query:'source.ip:%20"10.142.0.9"')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, + '/app/security/network/flows/?timerange=(global:(linkTo:!(),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(),timerange:(from:1564776209186,kind:absolute,to:1564779809186)))', + urlKqlNetworkNetwork: `/app/security/network/flows/?query=(language:kuery,query:'source.ip:%20"10.142.0.9"')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, + urlKqlNetworkHosts: `/app/security/network/flows/?query=(language:kuery,query:'source.ip:%20"10.142.0.9"')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, + urlKqlHostsNetwork: `/app/security/hosts/allHosts?query=(language:kuery,query:'source.ip:%20"10.142.0.9"')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, + urlKqlHostsHosts: `/app/security/hosts/allHosts?query=(language:kuery,query:'source.ip:%20"10.142.0.9"')&timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))`, urlHost: - '/app/security#/hosts/authentications?timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))', + '/app/security/hosts/authentications?timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1564691609186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1564691609186)))', urlHostNew: - '/app/security#/hosts/authentications?timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))', + '/app/security/hosts/authentications?timerange=(global:(linkTo:!(timeline),timerange:(from:1564689809186,kind:absolute,to:1577914409186)),timeline:(linkTo:!(global),timerange:(from:1564689809186,kind:absolute,to:1577914409186)))', }; diff --git a/x-pack/plugins/security_solution/public/alerts/components/activity_monitor/columns.tsx b/x-pack/plugins/security_solution/public/alerts/components/activity_monitor/columns.tsx deleted file mode 100644 index 51a5397637e7ca..00000000000000 --- a/x-pack/plugins/security_solution/public/alerts/components/activity_monitor/columns.tsx +++ /dev/null @@ -1,97 +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. - */ - -/* eslint-disable react/display-name */ - -import { - EuiIconTip, - EuiLink, - EuiTextColor, - EuiBasicTableColumn, - EuiTableActionsColumnType, -} from '@elastic/eui'; -import React from 'react'; -import { getEmptyTagValue } from '../../../common/components/empty_value'; -import { ColumnTypes } from './types'; - -const actions: EuiTableActionsColumnType['actions'] = [ - { - available: (item: ColumnTypes) => item.status === 'Running', - description: 'Stop', - icon: 'stop', - isPrimary: true, - name: 'Stop', - onClick: () => {}, - type: 'icon', - }, - { - available: (item: ColumnTypes) => item.status === 'Stopped', - description: 'Resume', - icon: 'play', - isPrimary: true, - name: 'Resume', - onClick: () => {}, - type: 'icon', - }, -]; - -// Michael: Are we able to do custom, in-table-header filters, as shown in my wireframes? -export const columns: Array> = [ - { - field: 'rule' as const, - name: 'Rule', - render: (value: ColumnTypes['rule'], _: ColumnTypes) => ( - {value.name} - ), - sortable: true, - truncateText: true, - }, - { - field: 'ran' as const, - name: 'Ran', - render: (value: ColumnTypes['ran'], _: ColumnTypes) => '--', - sortable: true, - truncateText: true, - }, - { - field: 'lookedBackTo' as const, - name: 'Looked back to', - render: (value: ColumnTypes['lookedBackTo'], _: ColumnTypes) => '--', - sortable: true, - truncateText: true, - }, - { - field: 'status' as const, - name: 'Status', - sortable: true, - truncateText: true, - }, - { - field: 'response' as const, - name: 'Response', - render: (value: ColumnTypes['response'], _: ColumnTypes) => { - return value === undefined ? ( - getEmptyTagValue() - ) : ( - <> - {value === 'Fail' ? ( - - {value} - - ) : ( - {value} - )} - - ); - }, - sortable: true, - truncateText: true, - }, - { - actions, - width: '40px', - }, -]; diff --git a/x-pack/plugins/security_solution/public/alerts/components/activity_monitor/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/activity_monitor/index.test.tsx deleted file mode 100644 index c5a4057b64ea70..00000000000000 --- a/x-pack/plugins/security_solution/public/alerts/components/activity_monitor/index.test.tsx +++ /dev/null @@ -1,18 +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 React from 'react'; -import { shallow } from 'enzyme'; - -import { ActivityMonitor } from './index'; - -describe('activity_monitor', () => { - it('renders correctly', () => { - const wrapper = shallow(); - - expect(wrapper.find('[title="Activity monitor"]')).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/alerts/components/activity_monitor/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/activity_monitor/index.tsx deleted file mode 100644 index c2b6b0f025e1d6..00000000000000 --- a/x-pack/plugins/security_solution/public/alerts/components/activity_monitor/index.tsx +++ /dev/null @@ -1,320 +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 { EuiBasicTable, EuiPanel, EuiSpacer } from '@elastic/eui'; -import React, { useState, useCallback } from 'react'; -import { HeaderSection } from '../../../common/components/header_section'; -import { - UtilityBar, - UtilityBarAction, - UtilityBarGroup, - UtilityBarSection, - UtilityBarText, -} from '../../../common/components/utility_bar'; -import { columns } from './columns'; -import { ColumnTypes, PageTypes, SortTypes } from './types'; - -export const ActivityMonitor = React.memo(() => { - const sampleTableData: ColumnTypes[] = [ - { - id: 1, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Running', - }, - { - id: 2, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Stopped', - }, - { - id: 3, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Fail', - }, - { - id: 4, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 5, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 6, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 7, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 8, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 9, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 10, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 11, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 12, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 13, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 14, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 15, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 16, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 17, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 18, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 19, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 20, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 21, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - ]; - - const [itemsTotalState] = useState(sampleTableData.length); - const [pageState, setPageState] = useState({ index: 0, size: 20 }); - // const [selectedState, setSelectedState] = useState([]); - const [sortState, setSortState] = useState({ field: 'ran', direction: 'desc' }); - - const handleChange = useCallback( - ({ page, sort }: { page?: PageTypes; sort?: SortTypes }) => { - setPageState(page!); - setSortState(sort!); - }, - [setPageState, setSortState] - ); - - return ( - <> - - - - - - - - {'Showing: 39 activites'} - - - - {'Selected: 2 activities'} - - {'Stop selected'} - - - - {'Clear 7 filters'} - - - - { - // @ts-ignore `Columns` interface differs from EUI's `column` type and is used all over this plugin, so ignore the differences instead of refactoring a lot of code - } - item.status !== 'Completed', - selectableMessage: (selectable: boolean) => - selectable ? '' : 'Completed runs cannot be acted upon', - onSelectionChange: (selectedItems: ColumnTypes[]) => { - // setSelectedState(selectedItems); - }, - }} - sorting={{ - sort: sortState, - }} - /> - - - ); -}); -ActivityMonitor.displayName = 'ActivityMonitor'; diff --git a/x-pack/plugins/security_solution/public/alerts/components/activity_monitor/types.ts b/x-pack/plugins/security_solution/public/alerts/components/activity_monitor/types.ts deleted file mode 100644 index 816992ff940dd9..00000000000000 --- a/x-pack/plugins/security_solution/public/alerts/components/activity_monitor/types.ts +++ /dev/null @@ -1,29 +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. - */ - -export interface RuleTypes { - href: string; - name: string; -} - -export interface ColumnTypes { - id: number; - rule: RuleTypes; - ran: string; - lookedBackTo: string; - status: string; - response?: string | undefined; -} - -export interface PageTypes { - index: number; - size: number; -} - -export interface SortTypes { - field: keyof ColumnTypes; - direction: 'asc' | 'desc'; -} diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/index.test.tsx index 3376df76ac6ece..db783e6cc2f88f 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/index.test.tsx @@ -9,6 +9,15 @@ import { shallow } from 'enzyme'; import { AlertsHistogramPanel } from './index'; +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); + return { + ...originalModule, + createHref: jest.fn(), + useHistory: jest.fn(), + }; +}); + jest.mock('../../../common/lib/kibana'); jest.mock('../../../common/components/navigation/use_get_url_search'); diff --git a/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/index.tsx index ed98a377755760..e6eb8afc1658f4 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/alerts_histogram_panel/index.tsx @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ import { Position } from '@elastic/charts'; -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSelect, EuiPanel } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiSelect, EuiPanel } from '@elastic/eui'; import numeral from '@elastic/numeral'; import React, { memo, useCallback, useMemo, useState, useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; import styled from 'styled-components'; import { isEmpty } from 'lodash/fp'; import uuid from 'uuid'; @@ -18,19 +19,19 @@ import { escapeDataProviderId } from '../../../common/components/drag_and_drop/h import { HeaderSection } from '../../../common/components/header_section'; import { Filter, esQuery, Query } from '../../../../../../../src/plugins/data/public'; import { useQueryAlerts } from '../../containers/detection_engine/alerts/use_query'; -import { getDetectionEngineUrl } from '../../../common/components/link_to'; +import { getDetectionEngineUrl, useFormatUrl } from '../../../common/components/link_to'; import { defaultLegendColors } from '../../../common/components/matrix_histogram/utils'; import { InspectButtonContainer } from '../../../common/components/inspect'; -import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; import { MatrixLoader } from '../../../common/components/matrix_histogram/matrix_loader'; import { MatrixHistogramOption } from '../../../common/components/matrix_histogram/types'; import { useKibana, useUiSetting$ } from '../../../common/lib/kibana'; -import { navTabs } from '../../../app/home/home_navigations'; import { alertsHistogramOptions } from './config'; import { formatAlertsData, getAlertsHistogramQuery, showInitialLoadingSpinner } from './helpers'; import { AlertsHistogram } from './alerts_histogram'; import * as i18n from './translations'; import { RegisterQuery, AlertsHistogramOption, AlertsAggregation, AlertsTotal } from './types'; +import { LinkButton } from '../../../common/components/links'; +import { SecurityPageName } from '../../../app/types'; const DEFAULT_PANEL_HEIGHT = 300; @@ -102,6 +103,7 @@ export const AlertsHistogramPanel = memo( title = i18n.HISTOGRAM_HEADER, updateDateRange, }) => { + const history = useHistory(); // create a unique, but stable (across re-renders) query id const uniqueQueryId = useMemo(() => `${DETECTIONS_HISTOGRAM_ID}-${uuid.v4()}`, []); const [isInitialLoading, setIsInitialLoading] = useState(true); @@ -122,7 +124,7 @@ export const AlertsHistogramPanel = memo( signalIndexName ); const kibana = useKibana(); - const urlSearch = useGetUrlSearch(navTabs.detections); + const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.alerts); const totalAlerts = useMemo( () => @@ -142,6 +144,13 @@ export const AlertsHistogramPanel = memo( // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const goToDetectionEngine = useCallback( + (ev) => { + ev.preventDefault(); + history.push(getDetectionEngineUrl(urlSearch)); + }, + [history, urlSearch] + ); const formattedAlertsData = useMemo(() => formatAlertsData(alertsData), [alertsData]); const legendItems: LegendItem[] = useMemo( @@ -231,11 +240,13 @@ export const AlertsHistogramPanel = memo( if (showLinkToAlerts) { return ( - {i18n.VIEW_ALERTS} + + {i18n.VIEW_ALERTS} + ); } - }, [showLinkToAlerts, urlSearch]); + }, [showLinkToAlerts, goToDetectionEngine, formatUrl]); const titleText = useMemo(() => (onlyField == null ? title : i18n.TOP(onlyField)), [ onlyField, diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx index 8ace42fc5c3f90..89f6399071dd35 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx @@ -9,6 +9,19 @@ import { shallow } from 'enzyme'; import { PrePackagedRulesPrompt } from './load_empty_prompt'; +jest.mock('react-router-dom', () => { + const original = jest.requireActual('react-router-dom'); + + return { + ...original, + useHistory: () => ({ + useHistory: jest.fn(), + }), + }; +}); + +jest.mock('../../../../common/components/link_to'); + describe('PrePackagedRulesPrompt', () => { it('renders correctly', () => { const wrapper = shallow( diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/load_empty_prompt.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/load_empty_prompt.tsx index cd88c4ce72af80..d82b930210ecd9 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/load_empty_prompt.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/pre_packaged_rules/load_empty_prompt.tsx @@ -8,8 +8,12 @@ import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/e import React, { memo, useCallback } from 'react'; import styled from 'styled-components'; -import { DETECTION_ENGINE_PAGE_NAME } from '../../../../common/components/link_to/redirect_to_detection_engine'; +import { useHistory } from 'react-router-dom'; +import { getCreateRuleUrl } from '../../../../common/components/link_to/redirect_to_detection_engine'; import * as i18n from './translations'; +import { LinkButton } from '../../../../common/components/links'; +import { SecurityPageName } from '../../../../app/types'; +import { useFormatUrl } from '../../../../common/components/link_to'; const EmptyPrompt = styled(EuiEmptyPrompt)` align-self: center; /* Corrects horizontal centering in IE11 */ @@ -28,9 +32,20 @@ const PrePackagedRulesPromptComponent: React.FC = ( loading = false, userHasNoPermissions = true, }) => { + const history = useHistory(); const handlePreBuiltCreation = useCallback(() => { createPrePackagedRules(); }, [createPrePackagedRules]); + const { formatUrl } = useFormatUrl(SecurityPageName.alerts); + + const goToCreateRule = useCallback( + (ev) => { + ev.preventDefault(); + history.push(getCreateRuleUrl()); + }, + [history] + ); + return ( = ( - {i18n.CREATE_RULE_ACTION} - + } diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/index.tsx index 66f04c9bc6addf..7be50552c34d88 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/index.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/rule_actions_overflow/index.tsx @@ -25,7 +25,7 @@ import { duplicateRulesAction, } from '../../../pages/detection_engine/rules/all/actions'; import { GenericDownloader } from '../../../../common/components/generic_downloader'; -import { DETECTION_ENGINE_PAGE_NAME } from '../../../../common/components/link_to/redirect_to_detection_engine'; +import { getRulesUrl } from '../../../../common/components/link_to/redirect_to_detection_engine'; const MyEuiButtonIcon = styled(EuiButtonIcon)` &.euiButtonIcon { @@ -56,7 +56,7 @@ const RuleActionsOverflowComponent = ({ const [, dispatchToaster] = useStateToaster(); const onRuleDeletedCallback = useCallback(() => { - history.push(`/${DETECTION_ENGINE_PAGE_NAME}/rules`); + history.push(getRulesUrl()); }, [history]); const actions = useMemo( diff --git a/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/index.tsx b/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/index.tsx index 9334bd59bebf5a..061b8b0f8c36e3 100644 --- a/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/alerts/components/rules/step_rule_actions/index.tsx @@ -34,6 +34,8 @@ import { RuleActionsField } from '../rule_actions_field'; import { useKibana } from '../../../../common/lib/kibana'; import { getSchema } from './schema'; import * as I18n from './translations'; +import { APP_ID } from '../../../../../common/constants'; +import { SecurityPageName } from '../../../../app/types'; interface StepRuleActionsProps extends RuleStepProps { defaultValues?: ActionsStepRule | null; @@ -84,9 +86,16 @@ const StepRuleActionsComponent: FC = ({ schema, }); - const kibanaAbsoluteUrl = useMemo(() => application.getUrlForApp('siem', { absolute: true }), [ - application, - ]); + // TO DO need to make sure that logic is still valid + const kibanaAbsoluteUrl = useMemo(() => { + const url = application.getUrlForApp(`${APP_ID}:${SecurityPageName.alerts}`, { + absolute: true, + }); + if (url != null && url.includes('app/security/alerts')) { + return url.replace('app/security/alerts', 'app/security'); + } + return url; + }, [application]); const onSubmit = useCallback( async (enabled: boolean) => { diff --git a/x-pack/plugins/security_solution/public/alerts/index.ts b/x-pack/plugins/security_solution/public/alerts/index.ts index 1409ad4f54696c..a2e377a7329360 100644 --- a/x-pack/plugins/security_solution/public/alerts/index.ts +++ b/x-pack/plugins/security_solution/public/alerts/index.ts @@ -7,7 +7,7 @@ import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { getTimelinesInStorageByIds } from '../timelines/containers/local_storage'; import { TimelineIdLiteral, TimelineId } from '../../common/types/timeline'; -import { getAlertsRoutes } from './routes'; +import { AlertsRoutes } from './routes'; import { SecuritySubPlugin } from '../app/types'; const ALERTS_TIMELINE_IDS: TimelineIdLiteral[] = [ @@ -20,7 +20,7 @@ export class Alerts { public start(storage: Storage): SecuritySubPlugin { return { - routes: getAlertsRoutes(), + SubPluginRoutes: AlertsRoutes, storageTimelines: { timelineById: getTimelinesInStorageByIds(storage, ALERTS_TIMELINE_IDS), }, diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.test.tsx index de8a732839728d..62b942d03591c1 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.test.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.test.tsx @@ -15,12 +15,14 @@ import { useUserInfo } from '../../components/user_info'; jest.mock('../../components/user_info'); jest.mock('../../../common/lib/kibana'); +jest.mock('../../../common/components/link_to'); jest.mock('react-router-dom', () => { const originalModule = jest.requireActual('react-router-dom'); return { ...originalModule, useParams: jest.fn(), + useHistory: jest.fn(), }; }); diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx index 9e1c9db168bdaf..05a0b4441bb3a6 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/detection_engine.tsx @@ -4,11 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButton, EuiSpacer } from '@elastic/eui'; +import { EuiSpacer } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; import { StickyContainer } from 'react-sticky'; import { connect, ConnectedProps } from 'react-redux'; +import { useHistory } from 'react-router-dom'; +import { SecurityPageName } from '../../../app/types'; import { TimelineId } from '../../../../common/types/timeline'; import { GlobalTime } from '../../../common/containers/global_time'; import { @@ -37,6 +39,8 @@ import { DetectionEngineNoIndex } from './detection_engine_no_signal_index'; import { DetectionEngineHeaderPage } from '../../components/detection_engine_header_page'; import { DetectionEngineUserUnauthenticated } from './detection_engine_user_unauthenticated'; import * as i18n from './translations'; +import { LinkButton } from '../../../common/components/links'; +import { useFormatUrl } from '../../../common/components/link_to'; export const DetectionEnginePageComponent: React.FC = ({ filters, @@ -52,8 +56,9 @@ export const DetectionEnginePageComponent: React.FC = ({ signalIndexName, hasIndexWrite, } = useUserInfo(); - + const history = useHistory(); const [lastAlerts] = useAlertInfo({}); + const { formatUrl } = useFormatUrl(SecurityPageName.alerts); const updateDateRangeCallback = useCallback( ({ x }) => { @@ -66,6 +71,14 @@ export const DetectionEnginePageComponent: React.FC = ({ [setAbsoluteRangeDatePicker] ); + const goToRules = useCallback( + (ev) => { + ev.preventDefault(); + history.push(getRulesUrl()); + }, + [history] + ); + const indexToAdd = useMemo(() => (signalIndexName == null ? [] : [signalIndexName]), [ signalIndexName, ]); @@ -111,14 +124,15 @@ export const DetectionEnginePageComponent: React.FC = ({ } title={i18n.PAGE_TITLE} > - {i18n.BUTTON_MANAGE_RULES} - + @@ -161,7 +175,7 @@ export const DetectionEnginePageComponent: React.FC = ({ ); }} - + ); }; diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/index.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/index.tsx index 1f9b1373d404dc..914734aba4ec62 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/index.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/index.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { Redirect, Route, Switch, RouteComponentProps } from 'react-router-dom'; +import { Route, Switch, RouteComponentProps } from 'react-router-dom'; import { ManageUserInfo } from '../../components/user_info'; import { CreateRulePage } from './rules/create'; @@ -14,34 +14,26 @@ import { EditRulePage } from './rules/edit'; import { RuleDetailsPage } from './rules/details'; import { RulesPage } from './rules'; -const detectionEnginePath = `/:pageName(detections)`; - type Props = Partial> & { url: string }; const DetectionEngineContainerComponent: React.FC = () => ( - - + + - - + + - + - - + + - - + + - ( - - )} - /> ); diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/actions.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/actions.tsx index d414321e4b7752..5169ff009d63c2 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/actions.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/actions.tsx @@ -7,14 +7,14 @@ import * as H from 'history'; import React, { Dispatch } from 'react'; -import { DETECTION_ENGINE_PAGE_NAME } from '../../../../../common/components/link_to/redirect_to_detection_engine'; import { deleteRules, duplicateRules, enableRules, Rule, } from '../../../../../alerts/containers/detection_engine/rules'; -import { Action } from './reducer'; + +import { getEditRuleUrl } from '../../../../../common/components/link_to/redirect_to_detection_engine'; import { ActionToaster, @@ -26,9 +26,10 @@ import { track, METRIC_TYPE, TELEMETRY_EVENT } from '../../../../../common/lib/t import * as i18n from '../translations'; import { bucketRulesResponse } from './helpers'; +import { Action } from './reducer'; export const editRuleAction = (rule: Rule, history: H.History) => { - history.push(`/${DETECTION_ENGINE_PAGE_NAME}/rules/id/${rule.id}/edit`); + history.push(getEditRuleUrl(rule.id)); }; export const duplicateRulesAction = async ( diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/columns.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/columns.tsx index cf8a3fdb6f1e62..030f510b7aa37a 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/columns.tsx @@ -8,7 +8,6 @@ import { EuiBadge, - EuiLink, EuiBasicTableColumn, EuiTableActionsColumnType, EuiText, @@ -39,6 +38,7 @@ import { import { Action } from './reducer'; import { LocalizedDateTooltip } from '../../../../../common/components/localized_date_tooltip'; import * as detectionI18n from '../../translations'; +import { LinkAnchor } from '../../../../../common/components/links'; export const getActions = ( dispatch: React.Dispatch, @@ -87,10 +87,11 @@ export type RuleStatusRowItemType = RuleStatus & { }; export type RulesColumns = EuiBasicTableColumn | EuiTableActionsColumnType; export type RulesStatusesColumns = EuiBasicTableColumn; - +type FormatUrl = (path: string) => string; interface GetColumns { dispatch: React.Dispatch; dispatchToaster: Dispatch; + formatUrl: FormatUrl; history: H.History; hasMlPermissions: boolean; hasNoPermissions: boolean; @@ -102,6 +103,7 @@ interface GetColumns { export const getColumns = ({ dispatch, dispatchToaster, + formatUrl, history, hasMlPermissions, hasNoPermissions, @@ -113,9 +115,16 @@ export const getColumns = ({ field: 'name', name: i18n.COLUMN_RULE, render: (value: Rule['name'], item: Rule) => ( - + void }) => { + ev.preventDefault(); + history.push(getRuleDetailsUrl(item.id)); + }} + href={formatUrl(getRuleDetailsUrl(item.id))} + > {value} - + ), truncateText: true, width: '24%', @@ -222,16 +231,26 @@ export const getColumns = ({ return hasNoPermissions ? cols : [...cols, ...actions]; }; -export const getMonitoringColumns = (): RulesStatusesColumns[] => { +export const getMonitoringColumns = ( + history: H.History, + formatUrl: FormatUrl +): RulesStatusesColumns[] => { const cols: RulesStatusesColumns[] = [ { field: 'name', name: i18n.COLUMN_RULE, render: (value: RuleStatus['current_status']['status'], item: RuleStatusRowItemType) => { return ( - + void }) => { + ev.preventDefault(); + history.push(getRuleDetailsUrl(item.id)); + }} + href={formatUrl(getRuleDetailsUrl(item.id))} + > {value} - + ); }, truncateText: true, diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/index.test.tsx index 11909ae7d9c531..ae5c129befa5d7 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/index.test.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/index.test.tsx @@ -13,6 +13,19 @@ import { TestProviders } from '../../../../../common/mock'; import { wait } from '../../../../../common/lib/helpers'; import { AllRules } from './index'; +jest.mock('react-router-dom', () => { + const original = jest.requireActual('react-router-dom'); + + return { + ...original, + useHistory: () => ({ + useHistory: jest.fn(), + }), + }; +}); + +jest.mock('../../../../../common/components/link_to'); + jest.mock('./reducer', () => { return { allRulesReducer: jest.fn().mockReturnValue(() => ({ diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/index.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/index.tsx index 80ef5681189c50..65f7bb63c74e4f 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/index.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/all/index.tsx @@ -49,6 +49,8 @@ import { allRulesReducer, State } from './reducer'; import { RulesTableFilters } from './rules_table_filters/rules_table_filters'; import { useMlCapabilities } from '../../../../../common/components/ml_popover/hooks/use_ml_capabilities'; import { hasMlAdminPermissions } from '../../../../../../common/machine_learning/has_ml_admin_permissions'; +import { SecurityPageName } from '../../../../../app/types'; +import { useFormatUrl } from '../../../../../common/components/link_to'; const SORT_FIELD = 'enabled'; const initialState: State = { @@ -140,6 +142,7 @@ export const AllRules = React.memo( const [, dispatchToaster] = useStateToaster(); const mlCapabilities = useMlCapabilities(); const [allRulesTab, setAllRulesTab] = useState(AllRulesTabs.rules); + const { formatUrl } = useFormatUrl(SecurityPageName.alerts); // TODO: Refactor license check + hasMlAdminPermissions to common check const hasMlPermissions = @@ -225,6 +228,7 @@ export const AllRules = React.memo( return getColumns({ dispatch, dispatchToaster, + formatUrl, history, hasMlPermissions, hasNoPermissions, @@ -239,6 +243,7 @@ export const AllRules = React.memo( }, [ dispatch, dispatchToaster, + formatUrl, hasMlPermissions, history, loadingRuleIds, @@ -246,7 +251,10 @@ export const AllRules = React.memo( reFetchRulesData, ]); - const monitoringColumns = useMemo(() => getMonitoringColumns(), []); + const monitoringColumns = useMemo(() => getMonitoringColumns(history, formatUrl), [ + history, + formatUrl, + ]); useEffect(() => { if (reFetchRulesData != null) { diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/index.test.tsx index 7749e38578e906..b7a2d017c3666c 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/index.test.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/index.test.tsx @@ -11,6 +11,18 @@ import { TestProviders } from '../../../../../common/mock'; import { CreateRulePage } from './index'; import { useUserInfo } from '../../../../components/user_info'; +jest.mock('react-router-dom', () => { + const original = jest.requireActual('react-router-dom'); + + return { + ...original, + useHistory: () => ({ + useHistory: jest.fn(), + }), + }; +}); + +jest.mock('../../../../../common/components/link_to'); jest.mock('../../../../components/user_info'); describe('CreateRulePage', () => { diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/index.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/index.tsx index de7c99acee6e50..de3e23b11aaf8e 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/create/index.tsx @@ -6,12 +6,15 @@ import { EuiButtonEmpty, EuiAccordion, EuiHorizontalRule, EuiPanel, EuiSpacer } from '@elastic/eui'; import React, { useCallback, useRef, useState, useMemo } from 'react'; -import { Redirect } from 'react-router-dom'; +import { useHistory } from 'react-router-dom'; import styled, { StyledComponent } from 'styled-components'; import { usePersistRule } from '../../../../../alerts/containers/detection_engine/rules'; -import { DETECTION_ENGINE_PAGE_NAME } from '../../../../../common/components/link_to/redirect_to_detection_engine'; +import { + getRulesUrl, + getDetectionEngineUrl, +} from '../../../../../common/components/link_to/redirect_to_detection_engine'; import { WrapperPage } from '../../../../../common/components/wrapper_page'; import { displaySuccessToast, useStateToaster } from '../../../../../common/components/toasters'; import { SpyRoute } from '../../../../../common/utils/route/spy_routes'; @@ -35,6 +38,7 @@ import { } from '../types'; import { formatRule } from './helpers'; import * as i18n from './translations'; +import { SecurityPageName } from '../../../../../app/types'; const stepsRuleOrder = [ RuleStep.defineRule, @@ -117,6 +121,7 @@ const CreateRulePageComponent: React.FC = () => { // eslint-disable-next-line react-hooks/exhaustive-deps [stepsData.current['define-rule'].data] ); + const history = useHistory(); const setStepData = useCallback( (step: RuleStep, data: unknown, isValid: boolean) => { @@ -269,20 +274,27 @@ const CreateRulePageComponent: React.FC = () => { if (isSaved) { const ruleName = (stepsData.current[RuleStep.aboutRule].data as AboutStepRule).name; displaySuccessToast(i18n.SUCCESSFULLY_CREATED_RULES(ruleName), dispatchToaster); - return ; + history.replace(getRulesUrl()); + return null; } if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) { - return ; + history.replace(getDetectionEngineUrl()); + return null; } else if (userHasNoPermissions(canUserCRUD)) { - return ; + history.replace(getRulesUrl()); + return null; } return ( <> { - + ); }; diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.test.tsx index d755f972f29501..df6ea65ba52ba5 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.test.tsx @@ -14,6 +14,7 @@ import { setAbsoluteRangeDatePicker } from '../../../../../common/store/inputs/a import { useUserInfo } from '../../../../components/user_info'; import { useParams } from 'react-router-dom'; +jest.mock('../../../../../common/components/link_to'); jest.mock('../../../../components/user_info'); jest.mock('react-router-dom', () => { const originalModule = jest.requireActual('react-router-dom'); @@ -21,6 +22,7 @@ jest.mock('react-router-dom', () => { return { ...originalModule, useParams: jest.fn(), + useHistory: jest.fn(), }; }); diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.tsx index d06103bd887f75..90fd4bb225ec5a 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/details/index.tsx @@ -9,7 +9,6 @@ // TODO: Disabling complexity is temporary till this component is refactored as part of lists UI integration import { - EuiButton, EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem, @@ -20,7 +19,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { FC, memo, useCallback, useMemo, useState } from 'react'; -import { Redirect, useParams } from 'react-router-dom'; +import { useParams, useHistory } from 'react-router-dom'; import { StickyContainer } from 'react-sticky'; import { connect, ConnectedProps } from 'react-redux'; @@ -31,7 +30,7 @@ import { FormattedDate } from '../../../../../common/components/formatted_date'; import { getEditRuleUrl, getRulesUrl, - DETECTION_ENGINE_PAGE_NAME, + getDetectionEngineUrl, } from '../../../../../common/components/link_to/redirect_to_detection_engine'; import { SiemSearchBar } from '../../../../../common/components/search_bar'; import { WrapperPage } from '../../../../../common/components/wrapper_page'; @@ -73,6 +72,9 @@ import { FailureHistory } from './failure_history'; import { RuleStatus } from '../../../../components/rules//rule_status'; import { useMlCapabilities } from '../../../../../common/components/ml_popover/hooks/use_ml_capabilities'; import { hasMlAdminPermissions } from '../../../../../../common/machine_learning/has_ml_admin_permissions'; +import { SecurityPageName } from '../../../../../app/types'; +import { LinkButton } from '../../../../../common/components/links'; +import { useFormatUrl } from '../../../../../common/components/link_to'; import { ExceptionsViewer } from '../../../../../common/components/exceptions/viewer'; import { ExceptionListType } from '../../../../../common/components/exceptions/types'; @@ -130,6 +132,8 @@ export const RuleDetailsPageComponent: FC = ({ }; const [lastAlerts] = useAlertInfo({ ruleId }); const mlCapabilities = useMlCapabilities(); + const history = useHistory(); + const { formatUrl } = useFormatUrl(SecurityPageName.alerts); // TODO: Refactor license check + hasMlAdminPermissions to common check const hasMlPermissions = @@ -243,8 +247,17 @@ export const RuleDetailsPageComponent: FC = ({ [ruleEnabled, setRuleEnabled] ); + const goToEditRule = useCallback( + (ev) => { + ev.preventDefault(); + history.push(getEditRuleUrl(ruleId ?? '')); + }, + [history, ruleId] + ); + if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) { - return ; + history.replace(getDetectionEngineUrl()); + return null; } return ( @@ -266,6 +279,7 @@ export const RuleDetailsPageComponent: FC = ({ backOptions={{ href: getRulesUrl(), text: i18n.BACK_TO_RULES, + pageId: SecurityPageName.alerts, }} border subtitle={subTitle} @@ -309,13 +323,14 @@ export const RuleDetailsPageComponent: FC = ({ - {ruleI18n.EDIT_RULE_SETTINGS} - + = ({ }} - + ); }; diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/edit/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/edit/index.test.tsx index 91bc2ce7bce253..d754329bdd97f4 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/edit/index.test.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/edit/index.test.tsx @@ -12,12 +12,14 @@ import { EditRulePage } from './index'; import { useUserInfo } from '../../../../components/user_info'; import { useParams } from 'react-router-dom'; +jest.mock('../../../../../common/components/link_to'); jest.mock('../../../../components/user_info'); jest.mock('react-router-dom', () => { const originalModule = jest.requireActual('react-router-dom'); return { ...originalModule, + useHistory: jest.fn(), useParams: jest.fn(), }; }); diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/edit/index.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/edit/index.tsx index 73e76165183c5d..ba7444d8e8a52b 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/edit/index.tsx @@ -17,11 +17,14 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { FC, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { Redirect, useParams } from 'react-router-dom'; +import { useParams, useHistory } from 'react-router-dom'; import { useRule, usePersistRule } from '../../../../../alerts/containers/detection_engine/rules'; import { WrapperPage } from '../../../../../common/components/wrapper_page'; -import { DETECTION_ENGINE_PAGE_NAME } from '../../../../../common/components/link_to/redirect_to_detection_engine'; +import { + getRuleDetailsUrl, + getDetectionEngineUrl, +} from '../../../../../common/components/link_to/redirect_to_detection_engine'; import { displaySuccessToast, useStateToaster } from '../../../../../common/components/toasters'; import { SpyRoute } from '../../../../../common/utils/route/spy_routes'; import { useUserInfo } from '../../../../components/user_info'; @@ -48,6 +51,7 @@ import { ActionsStepRule, } from '../types'; import * as i18n from './translations'; +import { SecurityPageName } from '../../../../../app/types'; interface StepRuleForm { isValid: boolean; @@ -67,6 +71,7 @@ interface ActionsStepRuleForm extends StepRuleForm { } const EditRulePageComponent: FC = () => { + const history = useHistory(); const [, dispatchToaster] = useStateToaster(); const { loading: initLoading, @@ -328,6 +333,14 @@ const EditRulePageComponent: FC = () => { [selectedTab, stepsForm.current] ); + const goToDetailsRule = useCallback( + (ev) => { + ev.preventDefault(); + history.replace(getRuleDetailsUrl(ruleId ?? '')); + }, + [history, ruleId] + ); + useEffect(() => { if (rule != null) { const { aboutRuleData, defineRuleData, scheduleRuleData, ruleActionsData } = getStepsData({ @@ -348,13 +361,16 @@ const EditRulePageComponent: FC = () => { if (isSaved) { displaySuccessToast(i18n.SUCCESSFULLY_SAVED_RULE(rule?.name ?? ''), dispatchToaster); - return ; + history.replace(getRuleDetailsUrl(ruleId ?? '')); + return null; } if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) { - return ; + history.replace(getDetectionEngineUrl()); + return null; } else if (userHasNoPermissions(canUserCRUD)) { - return ; + history.replace(getRuleDetailsUrl(ruleId ?? '')); + return null; } return ( @@ -362,8 +378,9 @@ const EditRulePageComponent: FC = () => { { responsive={false} > - + {i18n.CANCEL} @@ -429,7 +446,7 @@ const EditRulePageComponent: FC = () => { - + ); }; diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/index.test.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/index.test.tsx index 29f875d113a424..7e6cd48ddc003c 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/index.test.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/index.test.tsx @@ -11,6 +11,18 @@ import { RulesPage } from './index'; import { useUserInfo } from '../../../components/user_info'; import { usePrePackagedRules } from '../../../../alerts/containers/detection_engine/rules'; +jest.mock('react-router-dom', () => { + const original = jest.requireActual('react-router-dom'); + + return { + ...original, + useHistory: () => ({ + useHistory: jest.fn(), + }), + }; +}); + +jest.mock('../../../../common/components/link_to'); jest.mock('../../../components/user_info'); jest.mock('../../../../alerts/containers/detection_engine/rules'); diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/index.tsx b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/index.tsx index 4d36a247817277..7684f710952e6f 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/index.tsx +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/index.tsx @@ -6,14 +6,13 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { useCallback, useRef, useState } from 'react'; -import { Redirect } from 'react-router-dom'; +import { useHistory } from 'react-router-dom'; import { usePrePackagedRules, importRules, } from '../../../../alerts/containers/detection_engine/rules'; import { - DETECTION_ENGINE_PAGE_NAME, getDetectionEngineUrl, getCreateRuleUrl, } from '../../../../common/components/link_to/redirect_to_detection_engine'; @@ -28,10 +27,14 @@ import { ReadOnlyCallOut } from '../../../components/rules/read_only_callout'; import { UpdatePrePackagedRulesCallOut } from '../../../components/rules/pre_packaged_rules/update_callout'; import { getPrePackagedRuleStatus, redirectToDetections, userHasNoPermissions } from './helpers'; import * as i18n from './translations'; +import { SecurityPageName } from '../../../../app/types'; +import { LinkButton } from '../../../../common/components/links'; +import { useFormatUrl } from '../../../../common/components/link_to'; type Func = (refreshPrePackagedRule?: boolean) => void; const RulesPageComponent: React.FC = () => { + const history = useHistory(); const [showImportModal, setShowImportModal] = useState(false); const refreshRulesData = useRef(null); const { @@ -63,6 +66,7 @@ const RulesPageComponent: React.FC = () => { rulesNotInstalled, rulesNotUpdated ); + const { formatUrl } = useFormatUrl(SecurityPageName.alerts); const handleRefreshRules = useCallback(async () => { if (refreshRulesData.current != null) { @@ -87,8 +91,17 @@ const RulesPageComponent: React.FC = () => { refreshRulesData.current = refreshRule; }, []); + const goToNewRule = useCallback( + (ev) => { + ev.preventDefault(); + history.push(getCreateRuleUrl()); + }, + [history] + ); + if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) { - return ; + history.replace(getDetectionEngineUrl()); + return null; } return ( @@ -114,6 +127,7 @@ const RulesPageComponent: React.FC = () => { backOptions={{ href: getDetectionEngineUrl(), text: i18n.BACK_TO_ALERTS, + pageId: SecurityPageName.alerts, }} title={i18n.PAGE_TITLE} > @@ -155,15 +169,16 @@ const RulesPageComponent: React.FC = () => { - {i18n.ADD_NEW_RULE} - + @@ -188,7 +203,7 @@ const RulesPageComponent: React.FC = () => { /> - + ); }; diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/utils.test.ts b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/utils.test.ts index 3d2f2dc03946ab..91de1467a83108 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/utils.test.ts +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/utils.test.ts @@ -6,6 +6,9 @@ import { getBreadcrumbs } from './utils'; +const getUrlForAppMock = (appId: string, options?: { path?: string; absolute?: boolean }) => + `${appId}${options?.path ?? ''}`; + describe('getBreadcrumbs', () => { it('returns default value for incorrect params', () => { expect( @@ -17,8 +20,9 @@ describe('getBreadcrumbs', () => { search: '', pathName: 'pathName', }, - [] + [], + getUrlForAppMock ) - ).toEqual([{ href: '#/link-to/detections', text: 'Alerts' }]); + ).toEqual([{ href: 'securitySolution:alerts', text: 'Alerts' }]); }); }); diff --git a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/utils.ts index e5cdbd7123ff4c..203a93acd849cd 100644 --- a/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/utils.ts +++ b/x-pack/plugins/security_solution/public/alerts/pages/detection_engine/rules/utils.ts @@ -9,7 +9,6 @@ import { isEmpty } from 'lodash/fp'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ChromeBreadcrumb } from '../../../../../../../../src/core/public'; import { - getDetectionEngineUrl, getDetectionEngineTabUrl, getRulesUrl, getRuleDetailsUrl, @@ -19,21 +18,28 @@ import { import * as i18nDetections from '../translations'; import * as i18nRules from './translations'; import { RouteSpyState } from '../../../../common/utils/route/types'; +import { GetUrlForApp } from '../../../../common/components/navigation/types'; +import { SecurityPageName } from '../../../../app/types'; +import { APP_ID } from '../../../../../common/constants'; -const getTabBreadcrumb = (pathname: string, search: string[]) => { - const tabPath = pathname.split('/')[2]; +const getTabBreadcrumb = (pathname: string, search: string[], getUrlForApp: GetUrlForApp) => { + const tabPath = pathname.split('/')[1]; if (tabPath === 'alerts') { return { text: i18nDetections.ALERT, - href: `${getDetectionEngineTabUrl(tabPath)}${!isEmpty(search[0]) ? search[0] : ''}`, + href: getUrlForApp(`${APP_ID}:${SecurityPageName.alerts}`, { + path: getDetectionEngineTabUrl(tabPath, !isEmpty(search[0]) ? search[0] : ''), + }), }; } if (tabPath === 'rules') { return { text: i18nRules.PAGE_TITLE, - href: `${getRulesUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, + href: getUrlForApp(`${APP_ID}:${SecurityPageName.alerts}`, { + path: getRulesUrl(!isEmpty(search[0]) ? search[0] : ''), + }), }; } }; @@ -44,15 +50,21 @@ const isRuleCreatePage = (pathname: string) => const isRuleEditPage = (pathname: string) => pathname.includes('/rules') && pathname.includes('/edit'); -export const getBreadcrumbs = (params: RouteSpyState, search: string[]): ChromeBreadcrumb[] => { +export const getBreadcrumbs = ( + params: RouteSpyState, + search: string[], + getUrlForApp: GetUrlForApp +): ChromeBreadcrumb[] => { let breadcrumb = [ { text: i18nDetections.PAGE_TITLE, - href: `${getDetectionEngineUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, + href: getUrlForApp(`${APP_ID}:${SecurityPageName.alerts}`, { + path: !isEmpty(search[0]) ? search[0] : '', + }), }, ]; - const tabBreadcrumb = getTabBreadcrumb(params.pathName, search); + const tabBreadcrumb = getTabBreadcrumb(params.pathName, search, getUrlForApp); if (tabBreadcrumb) { breadcrumb = [...breadcrumb, tabBreadcrumb]; @@ -63,7 +75,9 @@ export const getBreadcrumbs = (params: RouteSpyState, search: string[]): ChromeB ...breadcrumb, { text: params.state.ruleName, - href: `${getRuleDetailsUrl(params.detailName)}${!isEmpty(search[1]) ? search[1] : ''}`, + href: getUrlForApp(`${APP_ID}:${SecurityPageName.alerts}`, { + path: getRuleDetailsUrl(params.detailName, !isEmpty(search[0]) ? search[0] : ''), + }), }, ]; } @@ -73,7 +87,9 @@ export const getBreadcrumbs = (params: RouteSpyState, search: string[]): ChromeB ...breadcrumb, { text: i18nRules.ADD_PAGE_TITLE, - href: `${getCreateRuleUrl()}${!isEmpty(search[1]) ? search[1] : ''}`, + href: getUrlForApp(`${APP_ID}:${SecurityPageName.alerts}`, { + path: getCreateRuleUrl(!isEmpty(search[0]) ? search[0] : ''), + }), }, ]; } @@ -83,7 +99,9 @@ export const getBreadcrumbs = (params: RouteSpyState, search: string[]): ChromeB ...breadcrumb, { text: i18nRules.EDIT_PAGE_TITLE, - href: `${getEditRuleUrl(params.detailName)}${!isEmpty(search[1]) ? search[1] : ''}`, + href: getUrlForApp(`${APP_ID}:${SecurityPageName.alerts}`, { + path: getEditRuleUrl(params.detailName, !isEmpty(search[0]) ? search[0] : ''), + }), }, ]; } diff --git a/x-pack/plugins/security_solution/public/alerts/routes.tsx b/x-pack/plugins/security_solution/public/alerts/routes.tsx index 897ba3269546fc..8f542d1f886704 100644 --- a/x-pack/plugins/security_solution/public/alerts/routes.tsx +++ b/x-pack/plugins/security_solution/public/alerts/routes.tsx @@ -5,16 +5,19 @@ */ import React from 'react'; -import { Route } from 'react-router-dom'; +import { Route, Switch } from 'react-router-dom'; import { DetectionEngineContainer } from './pages/detection_engine'; -import { SiemPageName } from '../app/types'; +import { NotFoundPage } from '../app/404'; -export const getAlertsRoutes = () => [ - ( - - )} - />, -]; +export const AlertsRoutes: React.FC = () => ( + + ( + + )} + /> + } /> + +); diff --git a/x-pack/plugins/security_solution/public/app/app.tsx b/x-pack/plugins/security_solution/public/app/app.tsx index b5696c889d16e8..147efb9e0d2e20 100644 --- a/x-pack/plugins/security_solution/public/app/app.tsx +++ b/x-pack/plugins/security_solution/public/app/app.tsx @@ -4,90 +4,38 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createHashHistory, History } from 'history'; +import { History } from 'history'; import React, { memo, useMemo, FC } from 'react'; import { ApolloProvider } from 'react-apollo'; -import { Store } from 'redux'; +import { Store, Action } from 'redux'; import { Provider as ReduxStoreProvider } from 'react-redux'; import { ThemeProvider } from 'styled-components'; import { EuiErrorBoundary } from '@elastic/eui'; import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; -import { BehaviorSubject } from 'rxjs'; -import { pluck } from 'rxjs/operators'; -import { KibanaContextProvider, useKibana, useUiSetting$ } from '../common/lib/kibana'; -import { Storage } from '../../../../../src/plugins/kibana_utils/public'; - -import { DEFAULT_DARK_MODE } from '../../common/constants'; +import { DEFAULT_DARK_MODE, APP_NAME } from '../../common/constants'; import { ErrorToastDispatcher } from '../common/components/error_toast_dispatcher'; -import { compose } from '../common/lib/compose/kibana_compose'; -import { AppFrontendLibs, AppApolloClient } from '../common/lib/lib'; -import { StartServices } from '../types'; -import { PageRouter } from './routes'; -import { createStore, createInitialState } from '../common/store'; -import { GlobalToaster, ManageGlobalToaster } from '../common/components/toasters'; import { MlCapabilitiesProvider } from '../common/components/ml/permissions/ml_capabilities_provider'; -import { ManageGlobalTimeline } from '../timelines/components/manage_timeline'; +import { GlobalToaster, ManageGlobalToaster } from '../common/components/toasters'; +import { AppFrontendLibs } from '../common/lib/lib'; +import { KibanaContextProvider, useKibana, useUiSetting$ } from '../common/lib/kibana'; +import { State } from '../common/store'; import { ApolloClientContext } from '../common/utils/apollo_context'; -import { SecuritySubPlugins } from './types'; - -interface AppPluginRootComponentProps { - apolloClient: AppApolloClient; - history: History; - store: Store; - subPluginRoutes: React.ReactElement[]; - theme: any; // eslint-disable-line @typescript-eslint/no-explicit-any -} - -const AppPluginRootComponent: React.FC = ({ - apolloClient, - theme, - store, - subPluginRoutes, - history, -}) => ( - - - - - - - - - - - - - - - - - -); - -const AppPluginRoot = memo(AppPluginRootComponent); +import { ManageGlobalTimeline } from '../timelines/components/manage_timeline'; +import { StartServices } from '../types'; +import { PageRouter } from './routes'; interface StartAppComponent extends AppFrontendLibs { - subPlugins: SecuritySubPlugins; - storage: Storage; + children: React.ReactNode; + history: History; + store: Store; } -const StartAppComponent: FC = ({ subPlugins, storage, ...libs }) => { - const { routes: subPluginRoutes, store: subPluginsStore } = subPlugins; +const StartAppComponent: FC = ({ children, apolloClient, history, store }) => { const { i18n } = useKibana().services; - const history = createHashHistory(); - const libs$ = new BehaviorSubject(libs); - - const store = createStore( - createInitialState(subPluginsStore.initialState), - subPluginsStore.reducer, - libs$.pipe(pluck('apolloClient')), - storage, - subPluginsStore.middlewares - ); const [darkMode] = useUiSetting$(DEFAULT_DARK_MODE); const theme = useMemo( @@ -101,13 +49,23 @@ const StartAppComponent: FC = ({ subPlugins, storage, ...libs return ( - + + + + + + + + {children} + + + + + + + + + ); @@ -115,22 +73,30 @@ const StartAppComponent: FC = ({ subPlugins, storage, ...libs const StartApp = memo(StartAppComponent); -interface SiemAppComponentProps { +interface SecurityAppComponentProps extends AppFrontendLibs { + children: React.ReactNode; + history: History; services: StartServices; - subPlugins: SecuritySubPlugins; + store: Store; } -const SiemAppComponent: React.FC = ({ services, subPlugins }) => { - return ( - - - - ); -}; +const SecurityAppComponent: React.FC = ({ + children, + apolloClient, + history, + services, + store, +}) => ( + + + {children} + + +); -export const SiemApp = memo(SiemAppComponent); +export const SecurityApp = memo(SecurityAppComponent); diff --git a/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx b/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx index bb9e99326182fd..88e9d4179a9714 100644 --- a/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx +++ b/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx @@ -4,66 +4,68 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - getDetectionEngineUrl, - getOverviewUrl, - getNetworkUrl, - getTimelinesUrl, - getHostsUrl, - getCaseUrl, -} from '../../common/components/link_to'; import * as i18n from './translations'; -import { SiemPageName, SiemNavTab } from '../types'; -import { getManagementUrl } from '../../management'; +import { SecurityPageName } from '../types'; +import { SiemNavTab } from '../../common/components/navigation/types'; +import { + APP_OVERVIEW_PATH, + APP_ALERTS_PATH, + APP_HOSTS_PATH, + APP_NETWORK_PATH, + APP_TIMELINES_PATH, + APP_CASES_PATH, + APP_MANAGEMENT_PATH, +} from '../../../common/constants'; export const navTabs: SiemNavTab = { - [SiemPageName.overview]: { - id: SiemPageName.overview, + [SecurityPageName.overview]: { + id: SecurityPageName.overview, name: i18n.OVERVIEW, - href: getOverviewUrl(), + href: APP_OVERVIEW_PATH, disabled: false, urlKey: 'overview', }, - [SiemPageName.hosts]: { - id: SiemPageName.hosts, + [SecurityPageName.alerts]: { + id: SecurityPageName.alerts, + name: i18n.Alerts, + href: APP_ALERTS_PATH, + disabled: false, + urlKey: 'alerts', + }, + [SecurityPageName.hosts]: { + id: SecurityPageName.hosts, name: i18n.HOSTS, - href: getHostsUrl(), + href: APP_HOSTS_PATH, disabled: false, urlKey: 'host', }, - [SiemPageName.network]: { - id: SiemPageName.network, + [SecurityPageName.network]: { + id: SecurityPageName.network, name: i18n.NETWORK, - href: getNetworkUrl(), + href: APP_NETWORK_PATH, disabled: false, urlKey: 'network', }, - [SiemPageName.detections]: { - id: SiemPageName.detections, - name: i18n.DETECTION_ENGINE, - href: getDetectionEngineUrl(), - disabled: false, - urlKey: 'detections', - }, - [SiemPageName.timelines]: { - id: SiemPageName.timelines, + + [SecurityPageName.timelines]: { + id: SecurityPageName.timelines, name: i18n.TIMELINES, - href: getTimelinesUrl(), + href: APP_TIMELINES_PATH, disabled: false, urlKey: 'timeline', }, - [SiemPageName.case]: { - id: SiemPageName.case, + [SecurityPageName.case]: { + id: SecurityPageName.case, name: i18n.CASE, - href: getCaseUrl(null), + href: APP_CASES_PATH, disabled: false, urlKey: 'case', }, - [SiemPageName.management]: { - id: SiemPageName.management, + [SecurityPageName.management]: { + id: SecurityPageName.management, name: i18n.MANAGEMENT, - href: getManagementUrl({ name: 'default' }), + href: APP_MANAGEMENT_PATH, disabled: false, - urlKey: SiemPageName.management, + urlKey: SecurityPageName.management, }, }; diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx index b5aae0b28c8713..d8bdbd6e7ef5fb 100644 --- a/x-pack/plugins/security_solution/public/app/home/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/index.tsx @@ -5,7 +5,6 @@ */ import React, { useMemo } from 'react'; -import { Redirect, Route, Switch } from 'react-router-dom'; import styled from 'styled-components'; import { useThrottledResizeObserver } from '../../common/components/utils'; @@ -13,20 +12,14 @@ import { DragDropContextWrapper } from '../../common/components/drag_and_drop/dr import { Flyout } from '../../timelines/components/flyout'; import { HeaderGlobal } from '../../common/components/header_global'; import { HelpMenu } from '../../common/components/help_menu'; -import { LinkToPage } from '../../common/components/link_to'; -import { MlHostConditionalContainer } from '../../common/components/ml/conditional_links/ml_host_conditional_container'; -import { MlNetworkConditionalContainer } from '../../common/components/ml/conditional_links/ml_network_conditional_container'; import { AutoSaveWarningMsg } from '../../timelines/components/timeline/auto_save_warning'; import { UseUrlState } from '../../common/components/url_state'; import { WithSource, indicesExistOrDataTemporarilyUnavailable, } from '../../common/containers/source'; -import { SpyRoute } from '../../common/utils/route/spy_routes'; import { useShowTimeline } from '../../common/utils/timeline/use_show_timeline'; -import { NotFoundPage } from '../404'; import { navTabs } from './home_navigations'; -import { SiemPageName } from '../types'; const WrappedByAutoSizer = styled.div` height: 100%; @@ -52,10 +45,10 @@ const calculateFlyoutHeight = ({ }): number => Math.max(0, windowHeight - globalHeaderSize); interface HomePageProps { - subPlugins: JSX.Element[]; + children: React.ReactNode; } -export const HomePage: React.FC = ({ subPlugins }) => { +export const HomePage: React.FC = ({ children }) => { const { ref: measureRef, height: windowHeight = 0 } = useThrottledResizeObserver(); const flyoutHeight = useMemo( () => @@ -88,32 +81,13 @@ export const HomePage: React.FC = ({ subPlugins }) => { )} - - - {subPlugins} - } /> - ( - - )} - /> - ( - - )} - /> - } /> - + {children} )} - - ); }; diff --git a/x-pack/plugins/security_solution/public/app/home/translations.ts b/x-pack/plugins/security_solution/public/app/home/translations.ts index ccf927eba20c94..f5a08e6395f1f9 100644 --- a/x-pack/plugins/security_solution/public/app/home/translations.ts +++ b/x-pack/plugins/security_solution/public/app/home/translations.ts @@ -25,6 +25,10 @@ export const DETECTION_ENGINE = i18n.translate( } ); +export const Alerts = i18n.translate('xpack.securitySolution.navigation.alerts', { + defaultMessage: 'Alerts', +}); + export const TIMELINES = i18n.translate('xpack.securitySolution.navigation.timelines', { defaultMessage: 'Timelines', }); diff --git a/x-pack/plugins/security_solution/public/app/index.tsx b/x-pack/plugins/security_solution/public/app/index.tsx index 13d06bd1a98c24..0afd945af8597d 100644 --- a/x-pack/plugins/security_solution/public/app/index.tsx +++ b/x-pack/plugins/security_solution/public/app/index.tsx @@ -5,19 +5,35 @@ */ import React from 'react'; +import { Store, Action } from 'redux'; import { render, unmountComponentAtNode } from 'react-dom'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AppMountParameters } from '../../../../../src/core/public'; +import { State } from '../common/store'; import { StartServices } from '../types'; -import { SiemApp } from './app'; -import { SecuritySubPlugins } from './types'; +import { SecurityApp } from './app'; +import { AppFrontendLibs } from '../common/lib/lib'; -export const renderApp = ( - services: StartServices, - { element }: AppMountParameters, - subPlugins: SecuritySubPlugins -) => { - render(, element); +interface RenderAppProps extends AppFrontendLibs, AppMountParameters { + services: StartServices; + store: Store; + SubPluginRoutes: React.FC; +} + +export const renderApp = ({ + apolloClient, + element, + history, + services, + store, + SubPluginRoutes, +}: RenderAppProps) => { + render( + + + , + element + ); return () => unmountComponentAtNode(element); }; diff --git a/x-pack/plugins/security_solution/public/app/routes.tsx b/x-pack/plugins/security_solution/public/app/routes.tsx index d1395813d39f4a..fc0d4e1f4fa625 100644 --- a/x-pack/plugins/security_solution/public/app/routes.tsx +++ b/x-pack/plugins/security_solution/public/app/routes.tsx @@ -14,17 +14,17 @@ import { ManageRoutesSpy } from '../common/utils/route/manage_spy_routes'; import { RouteCapture } from '../common/components/endpoint/route_capture'; interface RouterProps { + children: React.ReactNode; history: History; - subPluginRoutes: JSX.Element[]; } -const PageRouterComponent: FC = ({ history, subPluginRoutes }) => ( +const PageRouterComponent: FC = ({ history, children }) => ( - + {children} diff --git a/x-pack/plugins/security_solution/public/app/types.ts b/x-pack/plugins/security_solution/public/app/types.ts index 7a905b35710c99..4bd888e87bbdc7 100644 --- a/x-pack/plugins/security_solution/public/app/types.ts +++ b/x-pack/plugins/security_solution/public/app/types.ts @@ -14,33 +14,20 @@ import { CombinedState, } from 'redux'; -import { NavTab } from '../common/components/navigation/types'; import { State, SubPluginsInitReducer } from '../common/store'; import { Immutable } from '../../common/endpoint/types'; import { AppAction } from '../common/store/actions'; import { TimelineState } from '../timelines/store/timeline/types'; -export enum SiemPageName { +export enum SecurityPageName { + alerts = 'alerts', overview = 'overview', hosts = 'hosts', network = 'network', - detections = 'detections', timelines = 'timelines', case = 'case', management = 'management', } - -export type SiemNavTabKey = - | SiemPageName.overview - | SiemPageName.hosts - | SiemPageName.network - | SiemPageName.detections - | SiemPageName.timelines - | SiemPageName.case - | SiemPageName.management; - -export type SiemNavTab = Record; - export interface SecuritySubPluginStore { initialState: Record; reducer: Record>; @@ -48,7 +35,7 @@ export interface SecuritySubPluginStore } export interface SecuritySubPlugin { - routes: React.ReactElement[]; + SubPluginRoutes: React.FC; storageTimelines?: Pick; } diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx index bbb96f433d3c8f..ed8ec432f7df50 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx @@ -29,6 +29,16 @@ const useGetCasesMock = useGetCases as jest.Mock; const useGetCasesStatusMock = useGetCasesStatus as jest.Mock; const useUpdateCasesMock = useUpdateCases as jest.Mock; +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); + return { + ...originalModule, + useHistory: jest.fn(), + }; +}); + +jest.mock('../../../common/components/link_to'); + describe('AllCases', () => { const dispatchResetIsDeleted = jest.fn(); const dispatchResetIsUpdated = jest.fn(); @@ -98,7 +108,7 @@ describe('AllCases', () => { ); expect(wrapper.find(`a[data-test-subj="case-details-link"]`).first().prop('href')).toEqual( - `#/link-to/case/${useGetCasesMockState.data.cases[0].id}?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)))` + `/${useGetCasesMockState.data.cases[0].id}` ); expect(wrapper.find(`a[data-test-subj="case-details-link"]`).first().text()).toEqual( useGetCasesMockState.data.cases[0].title diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx index d27f383fb94e33..2de957039efe6c 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx @@ -5,9 +5,9 @@ */ /* eslint-disable react-hooks/exhaustive-deps */ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useHistory } from 'react-router-dom'; import { EuiBasicTable, - EuiButton, EuiContextMenuPanel, EuiEmptyPrompt, EuiFlexGroup, @@ -27,7 +27,6 @@ import { useGetCases, UpdateCase } from '../../containers/use_get_cases'; import { useGetCasesStatus } from '../../containers/use_get_cases_status'; import { useDeleteCases } from '../../containers/use_delete_cases'; import { EuiBasicTableOnChange } from '../../../alerts/pages/detection_engine/rules/types'; -import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; import { Panel } from '../../../common/components/panel'; import { UtilityBar, @@ -36,13 +35,11 @@ import { UtilityBarSection, UtilityBarText, } from '../../../common/components/utility_bar'; -import { getCreateCaseUrl } from '../../../common/components/link_to'; +import { getCreateCaseUrl, useFormatUrl } from '../../../common/components/link_to'; import { getBulkItems } from '../bulk_actions'; import { CaseHeaderPage } from '../case_header_page'; import { ConfirmDeleteCaseModal } from '../confirm_delete_case'; import { OpenClosedStats } from '../open_closed_stats'; -import { navTabs } from '../../../app/home/home_navigations'; - import { getActions } from './actions'; import { CasesTableFilters } from './table_filters'; import { useUpdateCases } from '../../containers/use_bulk_update_case'; @@ -51,6 +48,8 @@ import { getActionLicenseError } from '../use_push_to_service/helpers'; import { CaseCallOut } from '../callout'; import { ConfigureCaseButton } from '../configure_cases/button'; import { ERROR_PUSH_SERVICE_CALLOUT_TITLE } from '../use_push_to_service/translations'; +import { LinkButton } from '../../../common/components/links'; +import { SecurityPageName } from '../../../app/types'; const Div = styled.div` margin-top: ${({ theme }) => theme.eui.paddingSizes.m}; @@ -88,7 +87,8 @@ interface AllCasesProps { } export const AllCases = React.memo( ({ onRowClick = () => {}, isModal = false, userCanCrud }) => { - const urlSearch = useGetUrlSearch(navTabs.case); + const history = useHistory(); + const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.case); const { actionLicense } = useGetActionLicense(); const { countClosedCases, @@ -231,6 +231,14 @@ export const AllCases = React.memo( [dispatchUpdateCaseProperty, fetchCasesStatus] ); + const goToCreateCase = useCallback( + (ev) => { + ev.preventDefault(); + history.push(getCreateCaseUrl(urlSearch)); + }, + [history, urlSearch] + ); + const actions = useMemo( () => getActions({ @@ -342,15 +350,16 @@ export const AllCases = React.memo( /> - {i18n.CREATE_TITLE} - + @@ -418,15 +427,16 @@ export const AllCases = React.memo( titleSize="xs" body={i18n.NO_CASES_BODY} actions={ - {i18n.ADD_NEW_CASE} - + } /> } diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases_modal/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases_modal/index.test.tsx index a24cb6a87de745..f4fd7cc67224ff 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases_modal/index.test.tsx @@ -15,6 +15,19 @@ import { useGetCasesStatus } from '../../containers/use_get_cases_status'; import { useUpdateCases } from '../../containers/use_bulk_update_case'; import { EuiTableRow } from '@elastic/eui'; +jest.mock('react-router-dom', () => { + const original = jest.requireActual('react-router-dom'); + + return { + ...original, + useHistory: () => ({ + useHistory: jest.fn(), + }), + }; +}); + +jest.mock('../../../common/components/link_to'); + jest.mock('../../containers/use_bulk_update_case'); jest.mock('../../containers/use_delete_cases'); jest.mock('../../containers/use_get_cases'); diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/actions.test.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/actions.test.tsx index 2641ac68cf952e..86ec7d79a3f75a 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/actions.test.tsx @@ -15,6 +15,17 @@ import * as i18n from './translations'; jest.mock('../../containers/use_delete_cases'); const useDeleteCasesMock = useDeleteCases as jest.Mock; +jest.mock('react-router-dom', () => { + const original = jest.requireActual('react-router-dom'); + + return { + ...original, + useHistory: () => ({ + useHistory: jest.fn(), + }), + }; +}); + describe('CaseView actions', () => { const handleOnDeleteConfirm = jest.fn(); const handleToggleModal = jest.fn(); diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/actions.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/actions.tsx index df1082ec48f914..6439972ccbb5a6 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/actions.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/actions.tsx @@ -6,11 +6,10 @@ import { isEmpty } from 'lodash/fp'; import React, { useMemo } from 'react'; -import { Redirect } from 'react-router-dom'; +import { useHistory } from 'react-router-dom'; import * as i18n from './translations'; import { useDeleteCases } from '../../containers/use_delete_cases'; import { ConfirmDeleteCaseModal } from '../confirm_delete_case'; -import { SiemPageName } from '../../../app/types'; import { PropertyActions } from '../property_actions'; import { Case } from '../../containers/types'; import { CaseService } from '../../containers/use_get_case_user_actions'; @@ -26,6 +25,7 @@ const CaseViewActionsComponent: React.FC = ({ currentExternalIncident, disabled = false, }) => { + const history = useHistory(); // Delete case const { handleToggleModal, @@ -69,7 +69,8 @@ const CaseViewActionsComponent: React.FC = ({ ); if (isDeleted) { - return ; + history.push('/'); + return null; } return ( <> diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx index bb63cf633747b3..3718249479b63f 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx @@ -37,6 +37,7 @@ import { useGetCaseUserActions } from '../../containers/use_get_case_user_action import { usePushToService } from '../use_push_to_service'; import { EditConnector } from '../edit_connector'; import { useConnectors } from '../../containers/configure/use_connectors'; +import { SecurityPageName } from '../../../app/types'; interface Props { caseId: string; @@ -70,7 +71,7 @@ export interface CaseProps extends Props { export const CaseComponent = React.memo( ({ caseId, caseData, fetchCase, updateCase, userCanCrud }) => { const basePath = window.location.origin + useBasePath(); - const caseLink = `${basePath}/app/security#/case/${caseId}`; + const caseLink = `${basePath}/app/security/cases/${caseId}`; const search = useGetUrlSearch(navTabs.case); const [initLoadingData, setInitLoadingData] = useState(true); const { @@ -252,6 +253,7 @@ export const CaseComponent = React.memo( href: getCaseUrl(search), text: i18n.BACK_TO_ALL, dataTestSubj: 'backToCases', + pageId: SecurityPageName.case, }), [search] ); @@ -356,7 +358,7 @@ export const CaseComponent = React.memo( - + ); } diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/button.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/button.test.tsx index aefe515f4bd4db..5e19211b470783 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/button.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/button.test.tsx @@ -12,6 +12,19 @@ import { ConfigureCaseButton, ConfigureCaseButtonProps } from './button'; import { TestProviders } from '../../../common/mock'; import { searchURL } from './__mock__'; +jest.mock('react-router-dom', () => { + const original = jest.requireActual('react-router-dom'); + + return { + ...original, + useHistory: () => ({ + useHistory: jest.fn(), + }), + }; +}); + +jest.mock('../../../common/components/link_to'); + describe('Configuration button', () => { let wrapper: ReactWrapper; const props: ConfigureCaseButtonProps = { @@ -35,7 +48,7 @@ describe('Configuration button', () => { test('it pass the correct props to the button', () => { expect(wrapper.find('[data-test-subj="configure-case-button"]').first().props()).toMatchObject({ - href: `#/link-to/case/configure${searchURL}`, + href: `/configure`, iconType: 'controlsHorizontal', isDisabled: false, 'aria-label': 'My label', diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/button.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/button.tsx index a6d78d4a2a6202..44767471dd9e78 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/button.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/button.tsx @@ -4,9 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButton, EuiToolTip } from '@elastic/eui'; -import React, { memo, useMemo } from 'react'; -import { getConfigureCasesUrl } from '../../../common/components/link_to'; +import { EuiToolTip } from '@elastic/eui'; +import React, { memo, useCallback, useMemo } from 'react'; +import { useHistory } from 'react-router-dom'; + +import { getConfigureCasesUrl, useFormatUrl } from '../../../common/components/link_to'; +import { LinkButton } from '../../../common/components/links'; +import { SecurityPageName } from '../../../app/types'; export interface ConfigureCaseButtonProps { label: string; @@ -25,19 +29,29 @@ const ConfigureCaseButtonComponent: React.FC = ({ titleTooltip, urlSearch, }: ConfigureCaseButtonProps) => { + const history = useHistory(); + const { formatUrl } = useFormatUrl(SecurityPageName.case); + const goToCaseConfigure = useCallback( + (ev) => { + ev.preventDefault(); + history.push(getConfigureCasesUrl(urlSearch)); + }, + [history, urlSearch] + ); const configureCaseButton = useMemo( () => ( - {label} - + ), - [label, isDisabled, urlSearch] + [label, isDisabled, formatUrl, goToCaseConfigure] ); return showToolTip ? ( { ); wrapper.find(`[data-test-subj="create-case-cancel"]`).first().simulate('click'); - expect(mockHistory.replace.mock.calls[0][0].pathname).toEqual(`/${SiemPageName.case}`); + expect(mockHistory.push).toHaveBeenCalledWith('/'); }); it('should redirect to new case when caseData is there', () => { const sampleId = '777777'; @@ -122,9 +122,7 @@ describe('Create case', () => { ); - expect(mockHistory.replace.mock.calls[0][0].pathname).toEqual( - `/${SiemPageName.case}/${sampleId}` - ); + expect(mockHistory.push).toHaveBeenNthCalledWith(1, '/777777'); }); it('should render spinner when loading', () => { diff --git a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx index 1b65b2582c7609..9f078c725c3cfd 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx @@ -13,7 +13,7 @@ import { EuiPanel, } from '@elastic/eui'; import styled, { css } from 'styled-components'; -import { Redirect } from 'react-router-dom'; +import { useHistory } from 'react-router-dom'; import { isEqual } from 'lodash/fp'; import { CasePostRequest } from '../../../../../case/common/api'; @@ -30,9 +30,9 @@ import { schema } from './schema'; import { InsertTimelinePopover } from '../../../timelines/components/timeline/insert_timeline_popover'; import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'; import * as i18n from '../../translations'; -import { SiemPageName } from '../../../app/types'; import { MarkdownEditorForm } from '../../../common/components//markdown_editor/form'; import { useGetTags } from '../../containers/use_get_tags'; +import { getCaseDetailsUrl } from '../../../common/components/link_to'; export const CommonUseField = getUseField({ component: Field }); @@ -61,8 +61,8 @@ const initialCaseValue: CasePostRequest = { }; export const Create = React.memo(() => { + const history = useHistory(); const { caseData, isLoading, postCase } = usePostCase(); - const [isCancel, setIsCancel] = useState(false); const { form } = useForm({ defaultValue: initialCaseValue, options: { stripEmptyFields: false }, @@ -98,15 +98,12 @@ export const Create = React.memo(() => { }, [form]); const handleSetIsCancel = useCallback(() => { - setIsCancel(true); - }, []); + history.push('/'); + }, [history]); if (caseData != null && caseData.id) { - return ; - } - - if (isCancel) { - return ; + history.push(getCaseDetailsUrl({ id: caseData.id })); + return null; } return ( diff --git a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.test.tsx index 4391db1a0a0a1a..f2de830a71644d 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.test.tsx @@ -14,6 +14,19 @@ import * as i18n from './translations'; import { useGetActionLicense } from '../../containers/use_get_action_license'; import { getKibanaConfigError, getLicenseError } from './helpers'; import { connectorsMock } from '../../containers/configure/mock'; + +jest.mock('react-router-dom', () => { + const original = jest.requireActual('react-router-dom'); + + return { + ...original, + useHistory: () => ({ + useHistory: jest.fn(), + }), + }; +}); + +jest.mock('../../../common/components/link_to'); jest.mock('../../containers/use_get_action_license'); jest.mock('../../containers/use_post_push_to_service'); jest.mock('../../containers/configure/api'); diff --git a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.tsx b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.tsx index f18870787ded1f..45b515ccacacdc 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.tsx @@ -4,21 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButton, EuiLink, EuiToolTip } from '@elastic/eui'; +import { EuiButton, EuiToolTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useCallback, useMemo } from 'react'; +import { useHistory } from 'react-router-dom'; import { Case } from '../../containers/types'; import { useGetActionLicense } from '../../containers/use_get_action_license'; import { usePostPushToService } from '../../containers/use_post_push_to_service'; -import { getConfigureCasesUrl } from '../../../common/components/link_to'; -import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; -import { navTabs } from '../../../app/home/home_navigations'; +import { getConfigureCasesUrl, useFormatUrl } from '../../../common/components/link_to'; import { CaseCallOut } from '../callout'; import { getLicenseError, getKibanaConfigError } from './helpers'; import * as i18n from './translations'; import { Connector } from '../../../../../case/common/api/cases'; import { CaseServices } from '../../containers/use_get_case_user_actions'; +import { LinkAnchor } from '../../../common/components/links'; +import { SecurityPageName } from '../../../app/types'; export interface UsePushToService { caseId: string; @@ -48,8 +49,8 @@ export const usePushToService = ({ userCanCrud, isValidConnector, }: UsePushToService): ReturnUsePushToService => { - const urlSearch = useGetUrlSearch(navTabs.case); - + const history = useHistory(); + const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.case); const { isLoading, postPushToService } = usePostPushToService(); const { isLoading: loadingLicense, actionLicense } = useGetActionLicense(); @@ -66,6 +67,14 @@ export const usePushToService = ({ } }, [caseId, caseServices, caseConnectorId, caseConnectorName, postPushToService, updateCase]); + const goToConfigureCases = useCallback( + (ev) => { + ev.preventDefault(); + history.push(getConfigureCasesUrl(urlSearch)); + }, + [history, urlSearch] + ); + const errorsMsg = useMemo(() => { let errors: Array<{ title: string; @@ -86,9 +95,13 @@ export const usePushToService = ({ id="xpack.securitySolution.case.caseView.pushToServiceDisableByNoConnectors" values={{ link: ( - + {i18n.LINK_CONNECTOR_CONFIGURE} - + ), }} /> diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.test.tsx index ae9f1ec7469e41..b2d8ce9c891bcb 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.test.tsx @@ -18,7 +18,7 @@ const onSaveContent = jest.fn(); const timelineId = '1e10f150-949b-11ea-b63c-2bc51864784c'; const defaultProps = { - content: `A link to a timeline [timeline](http://localhost:5601/app/security#/timelines?timeline=(id:'${timelineId}',isOpen:!t))`, + content: `A link to a timeline [timeline](http://localhost:5601/app/security/timelines?timeline=(id:'${timelineId}',isOpen:!t))`, id: 'markdown-id', isEditable: false, onChangeEditable, diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_title.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_title.tsx index 307790194421d3..0bd6100b05df6d 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_title.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_title.tsx @@ -23,7 +23,7 @@ import { LocalizedDateTooltip } from '../../../common/components/localized_date_ import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; import { navTabs } from '../../../app/home/home_navigations'; import { PropertyActions } from '../property_actions'; -import { SiemPageName } from '../../../app/types'; +import { SecurityPageName } from '../../../app/types'; import * as i18n from './translations'; const MySpinner = styled(EuiLoadingSpinner)` @@ -94,7 +94,7 @@ export const UserActionTitle = ({ const handleAnchorLink = useCallback(() => { copy( - `${window.location.origin}${window.location.pathname}#${SiemPageName.case}/${caseId}/${id}${urlSearch}` + `${window.location.origin}${window.location.pathname}#${SecurityPageName.case}/${caseId}/${id}${urlSearch}` ); }, [caseId, id, urlSearch]); diff --git a/x-pack/plugins/security_solution/public/cases/index.ts b/x-pack/plugins/security_solution/public/cases/index.ts index 1eb8c82532e217..3b3130152f3838 100644 --- a/x-pack/plugins/security_solution/public/cases/index.ts +++ b/x-pack/plugins/security_solution/public/cases/index.ts @@ -5,14 +5,14 @@ */ import { SecuritySubPlugin } from '../app/types'; -import { getCasesRoutes } from './routes'; +import { CasesRoutes } from './routes'; export class Cases { public setup() {} public start(): SecuritySubPlugin { return { - routes: getCasesRoutes(), + SubPluginRoutes: CasesRoutes, }; } } diff --git a/x-pack/plugins/security_solution/public/cases/pages/case.tsx b/x-pack/plugins/security_solution/public/cases/pages/case.tsx index 03ebec34c2cdd6..eb6da9579e4e8b 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/case.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/case.tsx @@ -13,6 +13,7 @@ import { AllCases } from '../components/all_cases'; import { savedObjectReadOnly, CaseCallOut } from '../components/callout'; import { CaseSavedObjectNoPermissions } from './saved_object_no_permissions'; +import { SecurityPageName } from '../../app/types'; export const CasesPage = React.memo(() => { const userPermissions = useGetUserSavedObjectPermissions(); @@ -28,7 +29,7 @@ export const CasesPage = React.memo(() => { )} - + ) : ( diff --git a/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx b/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx index 780de303c02d34..43c51b32bce0f8 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx @@ -5,8 +5,10 @@ */ import React from 'react'; -import { useParams, Redirect } from 'react-router-dom'; +import { useParams, useHistory } from 'react-router-dom'; +import { SecurityPageName } from '../../app/types'; +import { SpyRoute } from '../../common/utils/route/spy_routes'; import { WrapperPage } from '../../common/components/wrapper_page'; import { useGetUrlSearch } from '../../common/components/navigation/use_get_url_search'; import { useGetUserSavedObjectPermissions } from '../../common/lib/kibana'; @@ -16,12 +18,14 @@ import { CaseView } from '../components/case_view'; import { savedObjectReadOnly, CaseCallOut } from '../components/callout'; export const CaseDetailsPage = React.memo(() => { + const history = useHistory(); const userPermissions = useGetUserSavedObjectPermissions(); const { detailName: caseId } = useParams(); const search = useGetUrlSearch(navTabs.case); if (userPermissions != null && !userPermissions.read) { - return ; + history.replace(getCaseUrl(search)); + return null; } return caseId != null ? ( @@ -35,6 +39,7 @@ export const CaseDetailsPage = React.memo(() => { )} + ) : null; }); diff --git a/x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx b/x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx index f70ff859e8e7d4..83354dabca1f2b 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx @@ -5,9 +5,10 @@ */ import React, { useMemo } from 'react'; -import { Redirect } from 'react-router-dom'; +import { useHistory } from 'react-router-dom'; import styled from 'styled-components'; +import { SecurityPageName } from '../../app/types'; import { getCaseUrl } from '../../common/components/link_to'; import { useGetUrlSearch } from '../../common/components/navigation/use_get_url_search'; import { WrapperPage } from '../../common/components/wrapper_page'; @@ -20,6 +21,7 @@ import { WhitePageWrapper, SectionWrapper } from '../components/wrappers'; import * as i18n from './translations'; const ConfigureCasesPageComponent: React.FC = () => { + const history = useHistory(); const userPermissions = useGetUserSavedObjectPermissions(); const search = useGetUrlSearch(navTabs.case); @@ -27,12 +29,14 @@ const ConfigureCasesPageComponent: React.FC = () => { () => ({ href: getCaseUrl(search), text: i18n.BACK_TO_ALL, + pageId: SecurityPageName.case, }), [search] ); if (userPermissions != null && !userPermissions.read) { - return ; + history.push(getCaseUrl(search)); + return null; } const HeaderWrapper = styled.div` @@ -51,7 +55,7 @@ const ConfigureCasesPageComponent: React.FC = () => { - + ); }; diff --git a/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx b/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx index c586a90e5ef9c3..672f44bfe275f2 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx @@ -5,8 +5,9 @@ */ import React, { useMemo } from 'react'; -import { Redirect } from 'react-router-dom'; +import { useHistory } from 'react-router-dom'; +import { SecurityPageName } from '../../app/types'; import { getCaseUrl } from '../../common/components/link_to'; import { useGetUrlSearch } from '../../common/components/navigation/use_get_url_search'; import { WrapperPage } from '../../common/components/wrapper_page'; @@ -18,6 +19,7 @@ import { Create } from '../components/create'; import * as i18n from './translations'; export const CreateCasePage = React.memo(() => { + const history = useHistory(); const userPermissions = useGetUserSavedObjectPermissions(); const search = useGetUrlSearch(navTabs.case); @@ -25,12 +27,14 @@ export const CreateCasePage = React.memo(() => { () => ({ href: getCaseUrl(search), text: i18n.BACK_TO_ALL, + pageId: SecurityPageName.case, }), [search] ); if (userPermissions != null && !userPermissions.crud) { - return ; + history.replace(getCaseUrl(search)); + return null; } return ( @@ -39,7 +43,7 @@ export const CreateCasePage = React.memo(() => { - + ); }); diff --git a/x-pack/plugins/security_solution/public/cases/pages/index.tsx b/x-pack/plugins/security_solution/public/cases/pages/index.tsx index 32f64d2690cba0..814be240644fc0 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/index.tsx @@ -7,13 +7,12 @@ import React from 'react'; import { Route, Switch } from 'react-router-dom'; -import { SiemPageName } from '../../app/types'; import { CaseDetailsPage } from './case_details'; import { CasesPage } from './case'; import { CreateCasePage } from './create_case'; import { ConfigureCasesPage } from './configure_cases'; -const casesPagePath = `/:pageName(${SiemPageName.case})`; +const casesPagePath = ''; const caseDetailsPagePath = `${casesPagePath}/:detailName`; const caseDetailsPagePathWithCommentId = `${casesPagePath}/:detailName/:commentId`; const createCasePagePath = `${casesPagePath}/create`; @@ -21,21 +20,21 @@ const configureCasesPagePath = `${casesPagePath}/configure`; const CaseContainerComponent: React.FC = () => ( - - - - + - + - + - + + + + ); diff --git a/x-pack/plugins/security_solution/public/cases/pages/utils.ts b/x-pack/plugins/security_solution/public/cases/pages/utils.ts index 0b60d66756d0c8..76308e6a1dd9b0 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/utils.ts +++ b/x-pack/plugins/security_solution/public/cases/pages/utils.ts @@ -8,17 +8,26 @@ import { isEmpty } from 'lodash/fp'; import { ChromeBreadcrumb } from 'src/core/public'; -import { getCaseDetailsUrl, getCaseUrl, getCreateCaseUrl } from '../../common/components/link_to'; +import { getCaseDetailsUrl, getCreateCaseUrl } from '../../common/components/link_to'; import { RouteSpyState } from '../../common/utils/route/types'; import * as i18n from './translations'; +import { GetUrlForApp } from '../../common/components/navigation/types'; +import { APP_ID } from '../../../common/constants'; +import { SecurityPageName } from '../../app/types'; -export const getBreadcrumbs = (params: RouteSpyState, search: string[]): ChromeBreadcrumb[] => { - const queryParameters = !isEmpty(search[0]) ? search[0] : null; +export const getBreadcrumbs = ( + params: RouteSpyState, + search: string[], + getUrlForApp: GetUrlForApp +): ChromeBreadcrumb[] => { + const queryParameters = !isEmpty(search[0]) ? search[0] : ''; let breadcrumb = [ { text: i18n.PAGE_TITLE, - href: getCaseUrl(queryParameters), + href: getUrlForApp(`${APP_ID}:${SecurityPageName.case}`, { + path: queryParameters, + }), }, ]; if (params.detailName === 'create') { @@ -26,7 +35,9 @@ export const getBreadcrumbs = (params: RouteSpyState, search: string[]): ChromeB ...breadcrumb, { text: i18n.CREATE_BC_TITLE, - href: getCreateCaseUrl(queryParameters), + href: getUrlForApp(`${APP_ID}:${SecurityPageName.case}`, { + path: getCreateCaseUrl(queryParameters), + }), }, ]; } else if (params.detailName != null) { @@ -34,7 +45,9 @@ export const getBreadcrumbs = (params: RouteSpyState, search: string[]): ChromeB ...breadcrumb, { text: params.state?.caseTitle ?? '', - href: getCaseDetailsUrl({ id: params.detailName, search: queryParameters }), + href: getUrlForApp(`${APP_ID}:${SecurityPageName.case}`, { + path: getCaseDetailsUrl({ id: params.detailName, search: queryParameters }), + }), }, ]; } diff --git a/x-pack/plugins/security_solution/public/cases/routes.tsx b/x-pack/plugins/security_solution/public/cases/routes.tsx index 698350e49bc3eb..c321f83c693f31 100644 --- a/x-pack/plugins/security_solution/public/cases/routes.tsx +++ b/x-pack/plugins/security_solution/public/cases/routes.tsx @@ -5,13 +5,16 @@ */ import React from 'react'; -import { Route } from 'react-router-dom'; +import { Route, Switch } from 'react-router-dom'; import { Case } from './pages'; -import { SiemPageName } from '../app/types'; +import { NotFoundPage } from '../app/404'; -export const getCasesRoutes = () => [ - - - , -]; +export const CasesRoutes: React.FC = () => ( + + + + + } /> + +); diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx index aa5efe3ccfe6a0..e60d876617dcaa 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx @@ -22,6 +22,8 @@ import { timelineDefaults, } from '../../../timelines/components/manage_timeline'; +jest.mock('../link_to'); + jest.mock('../../lib/kibana'); jest.mock('uuid', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx index cb2cbcc62aba14..30c7d2a742b571 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx @@ -15,6 +15,8 @@ import { mockBrowserFields } from '../../containers/source/mock'; import { defaultHeaders } from '../../mock/header'; import { useMountAppended } from '../../utils/use_mount_appended'; +jest.mock('../link_to'); + describe('EventDetails', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx index 9f60d16a6f6716..c0cc1c04488b23 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.test.tsx @@ -14,6 +14,8 @@ import { mockBrowserFields } from '../../containers/source/mock'; import { defaultHeaders } from '../../mock/header'; import { useMountAppended } from '../../utils/use_mount_appended'; +jest.mock('../link_to'); + describe('EventFieldsBrowser', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 9f1d71108e2f17..1645db371802c3 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -190,6 +190,7 @@ export const StatefulEventsViewer = connector( (prevProps, nextProps) => prevProps.id === nextProps.id && deepEqual(prevProps.columns, nextProps.columns) && + deepEqual(prevProps.defaultIndices, nextProps.defaultIndices) && deepEqual(prevProps.dataProviders, nextProps.dataProviders) && prevProps.deletedEventIds === nextProps.deletedEventIds && prevProps.end === nextProps.end && diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx index 79ed8bb37949f5..68a1e7220979da 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx @@ -243,7 +243,7 @@ const ExceptionsViewerComponent = ({ // Used in utility bar info text const ruleSettingsUrl = useMemo((): string => { return services.application.getUrlForApp( - `security#/detections/rules/id/${encodeURI(ruleId)}/edit` + `security/detections/rules/id/${encodeURI(ruleId)}/edit` ); }, [ruleId, services.application]); diff --git a/x-pack/plugins/security_solution/public/common/components/header_global/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/header_global/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 25374c63fa897d..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/header_global/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,19 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`HeaderGlobal it renders 1`] = ` - - - - - - - -`; diff --git a/x-pack/plugins/security_solution/public/common/components/header_global/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/header_global/index.test.tsx deleted file mode 100644 index 809f0eeb811f42..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/header_global/index.test.tsx +++ /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 { shallow } from 'enzyme'; -import React from 'react'; - -import '../../mock/match_media'; -import { HeaderGlobal } from './index'; - -jest.mock('react-router-dom', () => ({ - useLocation: () => ({ - pathname: '/app/siem#/hosts/allHosts', - hash: '', - search: '', - state: '', - }), - withRouter: () => jest.fn(), - generatePath: jest.fn(), -})); - -// Test will fail because we will to need to mock some core services to make the test work -// For now let's forget about SiemSearchBar -jest.mock('../search_bar', () => ({ - SiemSearchBar: () => null, -})); - -describe('HeaderGlobal', () => { - beforeEach(() => { - jest.resetAllMocks(); - }); - - test('it renders', () => { - const wrapper = shallow(); - - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx index c9085b5953817e..de19c1903586ae 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx @@ -4,21 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink } from '@elastic/eui'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; import { pickBy } from 'lodash/fp'; -import React from 'react'; +import React, { useCallback } from 'react'; import styled, { css } from 'styled-components'; -import { useLocation } from 'react-router-dom'; import { gutterTimeline } from '../../lib/helpers'; import { navTabs } from '../../../app/home/home_navigations'; -import { SiemPageName } from '../../../app/types'; -import { getOverviewUrl } from '../link_to'; +import { SecurityPageName } from '../../../app/types'; +import { getAppOverviewUrl } from '../link_to'; import { MlPopover } from '../ml_popover/ml_popover'; import { SiemNavigation } from '../navigation'; import * as i18n from './translations'; import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source'; -import { ADD_DATA_PATH } from '../../../../common/constants'; +import { useGetUrlSearch } from '../navigation/use_get_url_search'; +import { useKibana } from '../../lib/kibana'; +import { APP_ID, ADD_DATA_PATH, APP_ALERTS_PATH } from '../../../../common/constants'; +import { LinkAnchor } from '../links'; const Wrapper = styled.header` ${({ theme }) => css` @@ -39,7 +41,15 @@ interface HeaderGlobalProps { hideDetectionEngine?: boolean; } export const HeaderGlobal = React.memo(({ hideDetectionEngine = false }) => { - const currentLocation = useLocation(); + const search = useGetUrlSearch(navTabs.overview); + const { navigateToApp } = useKibana().services.application; + const goToOverview = useCallback( + (ev) => { + ev.preventDefault(); + navigateToApp(`${APP_ID}:${SecurityPageName.overview}`, { path: search }); + }, + [navigateToApp, search] + ); return ( @@ -50,9 +60,9 @@ export const HeaderGlobal = React.memo(({ hideDetectionEngine - + - + @@ -61,14 +71,14 @@ export const HeaderGlobal = React.memo(({ hideDetectionEngine display="condensed" navTabs={ hideDetectionEngine - ? pickBy((_, key) => key !== SiemPageName.detections, navTabs) + ? pickBy((_, key) => key !== SecurityPageName.alerts, navTabs) : navTabs } /> ) : ( key === SiemPageName.overview, navTabs)} + navTabs={pickBy((_, key) => key === SecurityPageName.overview, navTabs)} /> )} @@ -78,7 +88,7 @@ export const HeaderGlobal = React.memo(({ hideDetectionEngine {indicesExistOrDataTemporarilyUnavailable(indicesExist) && - currentLocation.pathname.includes(`/${SiemPageName.detections}/`) && ( + window.location.pathname.includes(APP_ALERTS_PATH) && ( diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/header_page/index.test.tsx index 29f227578ed968..30f510509913ab 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_page/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_page/index.test.tsx @@ -11,6 +11,20 @@ import React from 'react'; import { TestProviders } from '../../mock'; import { HeaderPage } from './index'; import { useMountAppended } from '../../utils/use_mount_appended'; +import { SecurityPageName } from '../../../app/types'; + +jest.mock('react-router-dom', () => { + const original = jest.requireActual('react-router-dom'); + + return { + ...original, + useHistory: () => ({ + useHistory: jest.fn(), + }), + }; +}); + +jest.mock('../link_to'); describe('HeaderPage', () => { const mount = useMountAppended(); @@ -34,7 +48,10 @@ describe('HeaderPage', () => { test('it renders the back link when provided', () => { const wrapper = mount( - + ); diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx index 3656b326e26d8a..62880e7510cd2b 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx @@ -5,13 +5,16 @@ */ import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiProgress } from '@elastic/eui'; -import React from 'react'; +import React, { useCallback } from 'react'; +import { useHistory } from 'react-router-dom'; import styled, { css } from 'styled-components'; import { LinkIcon, LinkIconProps } from '../link_icon'; import { Subtitle, SubtitleProps } from '../subtitle'; import { Title } from './title'; import { DraggableArguments, BadgeOptions, TitleProp } from './types'; +import { useFormatUrl } from '../link_to'; +import { SecurityPageName } from '../../../app/types'; interface HeaderProps { border?: boolean; @@ -61,6 +64,7 @@ interface BackOptions { href: LinkIconProps['href']; text: LinkIconProps['children']; dataTestSubj?: string; + pageId: SecurityPageName; } export interface HeaderPageProps extends HeaderProps { @@ -86,42 +90,56 @@ const HeaderPageComponent: React.FC = ({ title, titleNode, ...rest -}) => ( -
- - - {backOptions && ( - - - {backOptions.text} - - - )} - - {titleNode || ( - - )} +}) => { + const history = useHistory(); + const { formatUrl } = useFormatUrl(backOptions?.pageId ?? SecurityPageName.overview); + const goTo = useCallback( + (ev) => { + ev.preventDefault(); + if (backOptions) { + history.push(backOptions.href ?? ''); + } + }, + [backOptions, history] + ); + return ( + <Header border={border} {...rest}> + <EuiFlexGroup alignItems="center"> + <FlexItem> + {backOptions && ( + <LinkBack> + <LinkIcon + dataTestSubj={backOptions.dataTestSubj} + onClick={goTo} + href={formatUrl(backOptions.href ?? '')} + iconType="arrowLeft" + > + {backOptions.text} + </LinkIcon> + </LinkBack> + )} - {subtitle && <Subtitle data-test-subj="header-page-subtitle" items={subtitle} />} - {subtitle2 && <Subtitle data-test-subj="header-page-subtitle-2" items={subtitle2} />} - {border && isLoading && <EuiProgress size="xs" color="accent" />} - </FlexItem> + {titleNode || ( + <Title + draggableArguments={draggableArguments} + title={title} + badgeOptions={badgeOptions} + /> + )} - {children && ( - <FlexItem data-test-subj="header-page-supplements" grow={false}> - {children} + {subtitle && <Subtitle data-test-subj="header-page-subtitle" items={subtitle} />} + {subtitle2 && <Subtitle data-test-subj="header-page-subtitle-2" items={subtitle2} />} + {border && isLoading && <EuiProgress size="xs" color="accent" />} </FlexItem> - )} - </EuiFlexGroup> - </Header> -); + + {children && ( + <FlexItem data-test-subj="header-page-supplements" grow={false}> + {children} + </FlexItem> + )} + </EuiFlexGroup> + </Header> + ); +}; export const HeaderPage = React.memo(HeaderPageComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/__mocks__/index.ts b/x-pack/plugins/security_solution/public/common/components/link_to/__mocks__/index.ts new file mode 100644 index 00000000000000..6c9620e27fabf7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/link_to/__mocks__/index.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 { SecurityPageName } from '../../../../app/types'; + +export { getDetectionEngineUrl } from '../redirect_to_detection_engine'; +export { getAppOverviewUrl } from '../redirect_to_overview'; +export { getHostDetailsUrl, getHostsUrl } from '../redirect_to_hosts'; +export { getNetworkUrl, getIPDetailsUrl } from '../redirect_to_network'; +export { getTimelinesUrl, getTimelineTabsUrl } from '../redirect_to_timelines'; +export { + getCaseDetailsUrl, + getCaseUrl, + getCreateCaseUrl, + getConfigureCasesUrl, +} from '../redirect_to_case'; + +export const useFormatUrl = (page: SecurityPageName) => ({ + formatUrl: (path: string) => path, + search: '', +}); diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/helpers.test.ts b/x-pack/plugins/security_solution/public/common/components/link_to/helpers.test.ts index 14b367de674a27..97be9630c21984 100644 --- a/x-pack/plugins/security_solution/public/common/components/link_to/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/link_to/helpers.test.ts @@ -14,6 +14,6 @@ describe('appendSearch', () => { expect(appendSearch(undefined)).toEqual(''); }); test('should return parameter if parameter is defined', () => { - expect(appendSearch('helloWorld')).toEqual('helloWorld'); + expect(appendSearch('helloWorld')).toEqual('?helloWorld'); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/helpers.ts b/x-pack/plugins/security_solution/public/common/components/link_to/helpers.ts index 9d818ab3b64793..cf62547a0c7201 100644 --- a/x-pack/plugins/security_solution/public/common/components/link_to/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/link_to/helpers.ts @@ -4,4 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export const appendSearch = (search?: string) => (search != null ? `${search}` : ''); +import { isEmpty } from 'lodash/fp'; + +export const appendSearch = (search?: string) => + isEmpty(search) ? '' : `${search?.startsWith('?') ? search : `?${search}`}`; diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/index.ts b/x-pack/plugins/security_solution/public/common/components/link_to/index.ts index c35a60766d7bd9..140fa0e4601722 100644 --- a/x-pack/plugins/security_solution/public/common/components/link_to/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/link_to/index.ts @@ -4,25 +4,39 @@ * you may not use this file except in compliance with the Elastic License. */ -export { LinkToPage } from './link_to'; -export { - getDetectionEngineUrl, - RedirectToDetectionEnginePage, -} from './redirect_to_detection_engine'; -export { getOverviewUrl, RedirectToOverviewPage } from './redirect_to_overview'; +import { isEmpty } from 'lodash/fp'; +import { useCallback } from 'react'; +import { useHistory } from 'react-router-dom'; +import { SecurityPageName } from '../../../app/types'; +import { useGetUrlSearch } from '../navigation/use_get_url_search'; +import { navTabs } from '../../../app/home/home_navigations'; + +export { getDetectionEngineUrl } from './redirect_to_detection_engine'; +export { getAppOverviewUrl } from './redirect_to_overview'; export { getHostDetailsUrl, getHostsUrl } from './redirect_to_hosts'; -export { getNetworkUrl, getIPDetailsUrl, RedirectToNetworkPage } from './redirect_to_network'; -export { - getTimelinesUrl, - getTimelineTabsUrl, - RedirectToTimelinesPage, -} from './redirect_to_timelines'; +export { getNetworkUrl, getIPDetailsUrl } from './redirect_to_network'; +export { getTimelinesUrl, getTimelineTabsUrl } from './redirect_to_timelines'; export { getCaseDetailsUrl, getCaseUrl, getCreateCaseUrl, getConfigureCasesUrl, - RedirectToCasePage, - RedirectToCreatePage, - RedirectToConfigureCasesPage, } from './redirect_to_case'; + +export const useFormatUrl = (page: SecurityPageName) => { + const history = useHistory(); + const search = useGetUrlSearch(navTabs[page]); + const formatUrl = useCallback( + (path: string) => { + const pathArr = path.split('?'); + return history.createHref({ + pathname: pathArr[0], + search: isEmpty(pathArr[1]) + ? search + : `${pathArr[1]}${isEmpty(search) ? '' : `&${search}`}`, + }); + }, + [history, search] + ); + return { formatUrl, search }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/link_to.tsx b/x-pack/plugins/security_solution/public/common/components/link_to/link_to.tsx deleted file mode 100644 index 0294d175aef19f..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/link_to/link_to.tsx +++ /dev/null @@ -1,126 +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 React from 'react'; -import { match as RouteMatch, Redirect, Route, Switch } from 'react-router-dom'; - -import { SiemPageName } from '../../../app/types'; -import { HostsTableType } from '../../../hosts/store/model'; -import { - RedirectToCreateRulePage, - RedirectToDetectionEnginePage, - RedirectToEditRulePage, - RedirectToRuleDetailsPage, - RedirectToRulesPage, -} from './redirect_to_detection_engine'; -import { RedirectToHostsPage, RedirectToHostDetailsPage } from './redirect_to_hosts'; -import { RedirectToNetworkPage } from './redirect_to_network'; -import { RedirectToOverviewPage } from './redirect_to_overview'; -import { RedirectToTimelinesPage } from './redirect_to_timelines'; -import { - RedirectToCasePage, - RedirectToCreatePage, - RedirectToConfigureCasesPage, -} from './redirect_to_case'; -import { TimelineType } from '../../../../common/types/timeline'; -import { RedirectToManagementPage } from './redirect_to_management'; - -interface LinkToPageProps { - match: RouteMatch<{}>; -} - -export const LinkToPage = React.memo<LinkToPageProps>(({ match }) => ( - <Switch> - <Route - component={RedirectToOverviewPage} - path={`${match.url}/:pageName(${SiemPageName.overview})`} - /> - <Route - exact - component={RedirectToCasePage} - path={`${match.url}/:pageName(${SiemPageName.case})`} - /> - <Route - exact - component={RedirectToCreatePage} - path={`${match.url}/:pageName(${SiemPageName.case})/create`} - /> - <Route - exact - component={RedirectToConfigureCasesPage} - path={`${match.url}/:pageName(${SiemPageName.case})/configure`} - /> - <Route - component={RedirectToCasePage} - path={`${match.url}/:pageName(${SiemPageName.case})/:detailName`} - /> - <Route - component={RedirectToHostsPage} - exact - path={`${match.url}/:pageName(${SiemPageName.hosts})`} - /> - <Route - component={RedirectToHostsPage} - path={`${match.url}/:pageName(${SiemPageName.hosts})/:tabName(${HostsTableType.hosts}|${HostsTableType.authentications}|${HostsTableType.uncommonProcesses}|${HostsTableType.anomalies}|${HostsTableType.events}|${HostsTableType.alerts})`} - /> - <Route - component={RedirectToHostDetailsPage} - path={`${match.url}/:pageName(${SiemPageName.hosts})/:detailName/:tabName(${HostsTableType.authentications}|${HostsTableType.uncommonProcesses}|${HostsTableType.anomalies}|${HostsTableType.events}|${HostsTableType.alerts})`} - /> - <Route - component={RedirectToHostDetailsPage} - path={`${match.url}/:pageName(${SiemPageName.hosts})/:detailName`} - /> - <Route - component={RedirectToNetworkPage} - exact - path={`${match.url}/:pageName(${SiemPageName.network})`} - /> - <Route - component={RedirectToNetworkPage} - path={`${match.url}/:pageName(${SiemPageName.network})/ip/:detailName/:flowTarget`} - /> - <Route - component={RedirectToDetectionEnginePage} - exact - path={`${match.url}/:pageName(${SiemPageName.detections})`} - /> - <Route - component={RedirectToRulesPage} - exact - path={`${match.url}/:pageName(${SiemPageName.detections})/rules`} - /> - <Route - component={RedirectToCreateRulePage} - path={`${match.url}/:pageName(${SiemPageName.detections})/rules/create`} - /> - <Route - component={RedirectToRuleDetailsPage} - exact - path={`${match.url}/:pageName(${SiemPageName.detections})/rules/id/:detailName`} - /> - <Route - component={RedirectToEditRulePage} - path={`${match.url}/:pageName(${SiemPageName.detections})/rules/id/:detailName/edit`} - /> - <Route - component={RedirectToTimelinesPage} - exact - path={`${match.url}/:pageName(${SiemPageName.timelines})`} - /> - <Route - component={RedirectToTimelinesPage} - path={`${match.url}/:pageName(${SiemPageName.timelines})/:tabName(${TimelineType.default}|${TimelineType.template})`} - /> - <Route - component={RedirectToManagementPage} - path={`${match.url}/:pageName(${SiemPageName.management})`} - /> - <Redirect to="/" /> - </Switch> -)); - -LinkToPage.displayName = 'LinkToPage'; diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_case.tsx b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_case.tsx index e0c03519c6cbea..7005460999fc77 100644 --- a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_case.tsx +++ b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_case.tsx @@ -4,41 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { RouteComponentProps } from 'react-router-dom'; import { appendSearch } from './helpers'; -import { RedirectWrapper } from './redirect_wrapper'; -import { SiemPageName } from '../../../app/types'; -export type CaseComponentProps = RouteComponentProps<{ - detailName: string; -}>; +export const getCaseUrl = (search: string | null) => `${appendSearch(search ?? undefined)}`; -export const RedirectToCasePage = ({ - match: { - params: { detailName }, - }, -}: CaseComponentProps) => ( - <RedirectWrapper - to={detailName ? `/${SiemPageName.case}/${detailName}` : `/${SiemPageName.case}`} - /> -); +export const getCaseDetailsUrl = ({ id, search }: { id: string; search?: string | null }) => + `/${encodeURIComponent(id)}${appendSearch(search ?? undefined)}`; -export const RedirectToCreatePage = () => <RedirectWrapper to={`/${SiemPageName.case}/create`} />; -export const RedirectToConfigureCasesPage = () => ( - <RedirectWrapper to={`/${SiemPageName.case}/configure`} /> -); +export const getCreateCaseUrl = (search?: string | null) => + `/create${appendSearch(search ?? undefined)}`; -const baseCaseUrl = `#/link-to/${SiemPageName.case}`; - -export const getCaseUrl = (search: string | null) => - `${baseCaseUrl}${appendSearch(search ?? undefined)}`; - -export const getCaseDetailsUrl = ({ id, search }: { id: string; search: string | null }) => - `${baseCaseUrl}/${encodeURIComponent(id)}${appendSearch(search ?? undefined)}`; - -export const getCreateCaseUrl = (search: string | null) => - `${baseCaseUrl}/create${appendSearch(search ?? undefined)}`; - -export const getConfigureCasesUrl = (search: string) => - `${baseCaseUrl}/configure${appendSearch(search ?? undefined)}`; +export const getConfigureCasesUrl = (search?: string) => + `/configure${appendSearch(search ?? undefined)}`; diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_detection_engine.tsx b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_detection_engine.tsx index e1a021192ab88e..20e5367bfdde6d 100644 --- a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_detection_engine.tsx @@ -4,65 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { RouteComponentProps } from 'react-router-dom'; - import { appendSearch } from './helpers'; -import { RedirectWrapper } from './redirect_wrapper'; - -export type DetectionEngineComponentProps = RouteComponentProps<{ - detailName: string; - search: string; -}>; - -export const DETECTION_ENGINE_PAGE_NAME = 'detections'; - -export const RedirectToDetectionEnginePage = ({ - location: { search }, -}: DetectionEngineComponentProps) => { - const to = `/${DETECTION_ENGINE_PAGE_NAME}${search}`; - - return <RedirectWrapper to={to} />; -}; -export const RedirectToRulesPage = ({ location: { search } }: DetectionEngineComponentProps) => { - return <RedirectWrapper to={`/${DETECTION_ENGINE_PAGE_NAME}/rules${search}`} />; -}; +export const getDetectionEngineUrl = (search?: string) => `${appendSearch(search)}`; -export const RedirectToCreateRulePage = ({ - location: { search }, -}: DetectionEngineComponentProps) => { - return <RedirectWrapper to={`/${DETECTION_ENGINE_PAGE_NAME}/rules/create${search}`} />; -}; +export const getDetectionEngineTabUrl = (tabPath: string, search?: string) => + `/${tabPath}${appendSearch(search)}`; -export const RedirectToRuleDetailsPage = ({ - match: { - params: { detailName }, - }, - location: { search }, -}: DetectionEngineComponentProps) => { - return <RedirectWrapper to={`/${DETECTION_ENGINE_PAGE_NAME}/rules/id/${detailName}${search}`} />; -}; +export const getRulesUrl = (search?: string) => `/rules${appendSearch(search)}`; -export const RedirectToEditRulePage = ({ - match: { - params: { detailName }, - }, - location: { search }, -}: DetectionEngineComponentProps) => { - return ( - <RedirectWrapper to={`/${DETECTION_ENGINE_PAGE_NAME}/rules/id/${detailName}/edit${search}`} /> - ); -}; +export const getCreateRuleUrl = (search?: string) => `/rules/create${appendSearch(search)}`; -const baseDetectionEngineUrl = `#/link-to/${DETECTION_ENGINE_PAGE_NAME}`; +export const getRuleDetailsUrl = (detailName: string, search?: string) => + `/rules/id/${detailName}${appendSearch(search)}`; -export const getDetectionEngineUrl = (search?: string) => - `${baseDetectionEngineUrl}${appendSearch(search)}`; -export const getDetectionEngineTabUrl = (tabPath: string) => `${baseDetectionEngineUrl}/${tabPath}`; -export const getRulesUrl = () => `${baseDetectionEngineUrl}/rules`; -export const getCreateRuleUrl = () => `${baseDetectionEngineUrl}/rules/create`; -export const getRuleDetailsUrl = (detailName: string) => - `${baseDetectionEngineUrl}/rules/id/${detailName}`; -export const getEditRuleUrl = (detailName: string) => - `${baseDetectionEngineUrl}/rules/id/${detailName}/edit`; +export const getEditRuleUrl = (detailName: string, search?: string) => + `/rules/id/${detailName}/edit${appendSearch(search)}`; diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_hosts.tsx b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_hosts.tsx index 0cfe8e655e255d..5af24e5f7ce632 100644 --- a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_hosts.tsx +++ b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_hosts.tsx @@ -4,55 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { RouteComponentProps } from 'react-router-dom'; - import { HostsTableType } from '../../../hosts/store/model'; -import { SiemPageName } from '../../../app/types'; import { appendSearch } from './helpers'; -import { RedirectWrapper } from './redirect_wrapper'; - -export type HostComponentProps = RouteComponentProps<{ - detailName: string; - tabName: HostsTableType; - search: string; -}>; - -export const RedirectToHostsPage = ({ - match: { - params: { tabName }, - }, - location: { search }, -}: HostComponentProps) => { - const defaultSelectedTab = HostsTableType.hosts; - const selectedTab = tabName ? tabName : defaultSelectedTab; - const to = `/${SiemPageName.hosts}/${selectedTab}${search}`; - - return <RedirectWrapper to={to} />; -}; - -export const RedirectToHostDetailsPage = ({ - match: { - params: { detailName, tabName }, - }, - location: { search }, -}: HostComponentProps) => { - const defaultSelectedTab = HostsTableType.authentications; - const selectedTab = tabName ? tabName : defaultSelectedTab; - const to = `/${SiemPageName.hosts}/${detailName}/${selectedTab}${search}`; - return <RedirectWrapper to={to} />; -}; - -const baseHostsUrl = `#/link-to/${SiemPageName.hosts}`; -export const getHostsUrl = (search?: string) => `${baseHostsUrl}${appendSearch(search)}`; +export const getHostsUrl = (search?: string) => `${appendSearch(search)}`; export const getTabsOnHostsUrl = (tabName: HostsTableType, search?: string) => - `${baseHostsUrl}/${tabName}${appendSearch(search)}`; + `/${tabName}${appendSearch(search)}`; -export const getHostDetailsUrl = (detailName: string) => `${baseHostsUrl}/${detailName}`; +export const getHostDetailsUrl = (detailName: string, search?: string) => + `/${detailName}${appendSearch(search)}`; -export const getTabsOnHostDetailsUrl = (detailName: string, tabName: HostsTableType) => { - return `${baseHostsUrl}/${detailName}/${tabName}`; -}; +export const getTabsOnHostDetailsUrl = ( + detailName: string, + tabName: HostsTableType, + search?: string +) => `/${detailName}/${tabName}${appendSearch(search)}`; diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_management.tsx b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_management.tsx deleted file mode 100644 index 595c203993bb7e..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_management.tsx +++ /dev/null @@ -1,15 +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 React, { memo } from 'react'; -import { RedirectWrapper } from './redirect_wrapper'; -import { SiemPageName } from '../../../app/types'; - -export const RedirectToManagementPage = memo(() => { - return <RedirectWrapper to={`/${SiemPageName.management}`} />; -}); - -RedirectToManagementPage.displayName = 'RedirectToManagementPage'; diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_network.tsx b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_network.tsx index d72bacf511faa9..8e2b47bd91dbc4 100644 --- a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_network.tsx +++ b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_network.tsx @@ -4,39 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { RouteComponentProps } from 'react-router-dom'; - -import { SiemPageName } from '../../../app/types'; import { FlowTarget, FlowTargetSourceDest } from '../../../graphql/types'; import { appendSearch } from './helpers'; -import { RedirectWrapper } from './redirect_wrapper'; - -export type NetworkComponentProps = RouteComponentProps<{ - detailName?: string; - flowTarget?: string; - search: string; -}>; -export const RedirectToNetworkPage = ({ - match: { - params: { detailName, flowTarget }, - }, - location: { search }, -}: NetworkComponentProps) => ( - <RedirectWrapper - to={ - detailName - ? `/${SiemPageName.network}/ip/${detailName}/${flowTarget}${search}` - : `/${SiemPageName.network}${search}` - } - /> -); +export const getNetworkUrl = (search?: string) => `${appendSearch(search)}`; -const baseNetworkUrl = `#/link-to/${SiemPageName.network}`; -export const getNetworkUrl = (search?: string) => `${baseNetworkUrl}${appendSearch(search)}`; export const getIPDetailsUrl = ( detailName: string, - flowTarget?: FlowTarget | FlowTargetSourceDest -) => `${baseNetworkUrl}/ip/${detailName}/${flowTarget || FlowTarget.source}`; + flowTarget?: FlowTarget | FlowTargetSourceDest, + search?: string +) => `/ip/${detailName}/${flowTarget || FlowTarget.source}${appendSearch(search)}`; diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_overview.tsx b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_overview.tsx index 2043b820e6966c..d0cf46bd905210 100644 --- a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_overview.tsx +++ b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_overview.tsx @@ -4,17 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { RouteComponentProps } from 'react-router-dom'; -import { RedirectWrapper } from './redirect_wrapper'; -import { SiemPageName } from '../../../app/types'; +import { APP_OVERVIEW_PATH } from '../../../../common/constants'; +import { appendSearch } from './helpers'; -export type OverviewComponentProps = RouteComponentProps<{ - search: string; -}>; - -export const RedirectToOverviewPage = ({ location: { search } }: OverviewComponentProps) => ( - <RedirectWrapper to={`/${SiemPageName.overview}${search}`} /> -); - -export const getOverviewUrl = () => `#/link-to/${SiemPageName.overview}`; +export const getAppOverviewUrl = (search?: string) => `${APP_OVERVIEW_PATH}${appendSearch(search)}`; diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_timelines.tsx b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_timelines.tsx index 3562153bea646e..75a2fa1efa414f 100644 --- a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_timelines.tsx +++ b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_timelines.tsx @@ -4,37 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { RouteComponentProps } from 'react-router-dom'; - -import { SiemPageName } from '../../../app/types'; - +import { TimelineTypeLiteral } from '../../../../common/types/timeline'; import { appendSearch } from './helpers'; -import { RedirectWrapper } from './redirect_wrapper'; -import { TimelineTypeLiteral, TimelineType } from '../../../../common/types/timeline'; - -export type TimelineComponentProps = RouteComponentProps<{ - tabName: TimelineTypeLiteral; - search: string; -}>; - -export const RedirectToTimelinesPage = ({ - match: { - params: { tabName }, - }, - location: { search }, -}: TimelineComponentProps) => ( - <RedirectWrapper - to={ - tabName - ? `/${SiemPageName.timelines}/${tabName}${search}` - : `/${SiemPageName.timelines}/${TimelineType.default}${search}` - } - /> -); -export const getTimelinesUrl = (search?: string) => - `#/link-to/${SiemPageName.timelines}${appendSearch(search)}`; +export const getTimelinesUrl = (search?: string) => `${appendSearch(search)}`; export const getTimelineTabsUrl = (tabName: TimelineTypeLiteral, search?: string) => - `#/link-to/${SiemPageName.timelines}/${tabName}${appendSearch(search)}`; + `/${tabName}${appendSearch(search)}`; diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_wrapper.tsx deleted file mode 100644 index 39f24e8351f63d..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_wrapper.tsx +++ /dev/null @@ -1,18 +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 React from 'react'; -import { Redirect } from 'react-router-dom'; -import { useScrollToTop } from '../scroll_to_top'; - -export interface RedirectWrapperProps { - to: string; -} - -export const RedirectWrapper = ({ to }: RedirectWrapperProps) => { - useScrollToTop(); - return <Redirect to={to} />; -}; diff --git a/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx index 9eff86bffb369a..b6817c4cab1f21 100644 --- a/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx @@ -24,11 +24,20 @@ import { ExternalLink, } from '.'; +jest.mock('../link_to'); + jest.mock('../../../overview/components/events_by_dataset'); jest.mock('../../lib/kibana', () => { return { useUiSetting$: jest.fn(), + useKibana: () => ({ + services: { + application: { + navigateToApp: jest.fn(), + }, + }, + }), }; }); @@ -41,17 +50,13 @@ describe('Custom Links', () => { describe('HostDetailsLink', () => { test('should render valid link to Host Details with hostName as the display text', () => { const wrapper = mount(<HostDetailsLink hostName={hostName} />); - expect(wrapper.find('EuiLink').prop('href')).toEqual( - `#/link-to/hosts/${encodeURIComponent(hostName)}` - ); + expect(wrapper.find('EuiLink').prop('href')).toEqual(`/${encodeURIComponent(hostName)}`); expect(wrapper.text()).toEqual(hostName); }); test('should render valid link to Host Details with child text as the display text', () => { const wrapper = mount(<HostDetailsLink hostName={hostName}>{hostName}</HostDetailsLink>); - expect(wrapper.find('EuiLink').prop('href')).toEqual( - `#/link-to/hosts/${encodeURIComponent(hostName)}` - ); + expect(wrapper.find('EuiLink').prop('href')).toEqual(`/${encodeURIComponent(hostName)}`); expect(wrapper.text()).toEqual(hostName); }); }); @@ -60,7 +65,7 @@ describe('Custom Links', () => { test('should render valid link to IP Details with ipv4 as the display text', () => { const wrapper = mount(<IPDetailsLink ip={ipv4} />); expect(wrapper.find('EuiLink').prop('href')).toEqual( - `#/link-to/network/ip/${encodeURIComponent(ipv4)}/source` + `/ip/${encodeURIComponent(ipv4)}/source` ); expect(wrapper.text()).toEqual(ipv4); }); @@ -68,7 +73,7 @@ describe('Custom Links', () => { test('should render valid link to IP Details with child text as the display text', () => { const wrapper = mount(<IPDetailsLink ip={ipv4}>{hostName}</IPDetailsLink>); expect(wrapper.find('EuiLink').prop('href')).toEqual( - `#/link-to/network/ip/${encodeURIComponent(ipv4)}/source` + `/ip/${encodeURIComponent(ipv4)}/source` ); expect(wrapper.text()).toEqual(hostName); }); @@ -76,7 +81,7 @@ describe('Custom Links', () => { test('should render valid link to IP Details with ipv6 as the display text', () => { const wrapper = mount(<IPDetailsLink ip={ipv6} />); expect(wrapper.find('EuiLink').prop('href')).toEqual( - `#/link-to/network/ip/${encodeURIComponent(ipv6Encoded)}/source` + `/ip/${encodeURIComponent(ipv6Encoded)}/source` ); expect(wrapper.text()).toEqual(ipv6); }); diff --git a/x-pack/plugins/security_solution/public/common/components/links/index.tsx b/x-pack/plugins/security_solution/public/common/components/links/index.tsx index 637f0d8d530570..3b92faff915174 100644 --- a/x-pack/plugins/security_solution/public/common/components/links/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/links/index.tsx @@ -4,12 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiLink, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React, { useMemo } from 'react'; +import { + EuiButton, + EuiButtonProps, + EuiLink, + EuiLinkProps, + EuiToolTip, + EuiFlexGroup, + EuiFlexItem, + PropsForAnchor, + PropsForButton, +} from '@elastic/eui'; +import React, { useMemo, useCallback } from 'react'; import { isNil } from 'lodash/fp'; import styled from 'styled-components'; -import { IP_REPUTATION_LINKS_SETTING } from '../../../../common/constants'; +import { IP_REPUTATION_LINKS_SETTING, APP_ID } from '../../../../common/constants'; import { DefaultFieldRendererOverflow, DEFAULT_MORE_MAX_HEIGHT, @@ -20,27 +30,53 @@ import { getHostDetailsUrl, getIPDetailsUrl, getCreateCaseUrl, + useFormatUrl, } from '../link_to'; import { FlowTarget, FlowTargetSourceDest } from '../../../graphql/types'; -import { useUiSetting$ } from '../../lib/kibana'; +import { useUiSetting$, useKibana } from '../../lib/kibana'; import { isUrlInvalid } from '../../utils/validators'; import { ExternalLinkIcon } from '../external_link_icon'; -import { navTabs } from '../../../app/home/home_navigations'; -import { useGetUrlSearch } from '../navigation/use_get_url_search'; import * as i18n from './translations'; +import { SecurityPageName } from '../../../app/types'; export const DEFAULT_NUMBER_OF_LINK = 5; +export const LinkButton: React.FC< + PropsForButton<EuiButtonProps> | PropsForAnchor<EuiButtonProps> +> = ({ children, ...props }) => <EuiButton {...props}>{children}</EuiButton>; + +export const LinkAnchor: React.FC<EuiLinkProps> = ({ children, ...props }) => ( + <EuiLink {...props}>{children}</EuiLink> +); + // Internal Links const HostDetailsLinkComponent: React.FC<{ children?: React.ReactNode; hostName: string }> = ({ children, hostName, -}) => ( - <EuiLink href={getHostDetailsUrl(encodeURIComponent(hostName))}> - {children ? children : hostName} - </EuiLink> -); +}) => { + const { formatUrl, search } = useFormatUrl(SecurityPageName.hosts); + const { navigateToApp } = useKibana().services.application; + const goToHostDetails = useCallback( + (ev) => { + ev.preventDefault(); + navigateToApp(`${APP_ID}:${SecurityPageName.hosts}`, { + path: getHostDetailsUrl(encodeURIComponent(hostName), search), + }); + }, + [hostName, navigateToApp, search] + ); + + return ( + <LinkAnchor + onClick={goToHostDetails} + href={formatUrl(getHostDetailsUrl(encodeURIComponent(hostName)))} + > + {children ? children : hostName} + </LinkAnchor> + ); +}; +export const HostDetailsLink = React.memo(HostDetailsLinkComponent); const whitelistUrlSchemes = ['http://', 'https://']; export const ExternalLink = React.memo<{ @@ -75,17 +111,32 @@ export const ExternalLink = React.memo<{ ExternalLink.displayName = 'ExternalLink'; -export const HostDetailsLink = React.memo(HostDetailsLinkComponent); - const IPDetailsLinkComponent: React.FC<{ children?: React.ReactNode; ip: string; flowTarget?: FlowTarget | FlowTargetSourceDest; -}> = ({ children, ip, flowTarget = FlowTarget.source }) => ( - <EuiLink href={`${getIPDetailsUrl(encodeURIComponent(encodeIpv6(ip)), flowTarget)}`}> - {children ? children : ip} - </EuiLink> -); +}> = ({ children, ip, flowTarget = FlowTarget.source }) => { + const { formatUrl, search } = useFormatUrl(SecurityPageName.network); + const { navigateToApp } = useKibana().services.application; + const goToNetworkDetails = useCallback( + (ev) => { + ev.preventDefault(); + navigateToApp(`${APP_ID}:${SecurityPageName.network}`, { + path: getIPDetailsUrl(encodeURIComponent(encodeIpv6(ip)), flowTarget, search), + }); + }, + [flowTarget, ip, navigateToApp, search] + ); + + return ( + <LinkAnchor + onClick={goToNetworkDetails} + href={formatUrl(getIPDetailsUrl(encodeURIComponent(encodeIpv6(ip))))} + > + {children ? children : ip} + </LinkAnchor> + ); +}; export const IPDetailsLink = React.memo(IPDetailsLinkComponent); @@ -94,24 +145,49 @@ const CaseDetailsLinkComponent: React.FC<{ detailName: string; title?: string; }> = ({ children, detailName, title }) => { - const search = useGetUrlSearch(navTabs.case); + const { formatUrl, search } = useFormatUrl(SecurityPageName.case); + const { navigateToApp } = useKibana().services.application; + const goToCaseDetails = useCallback( + (ev) => { + ev.preventDefault(); + navigateToApp(`${APP_ID}:${SecurityPageName.case}`, { + path: getCaseDetailsUrl({ id: detailName, search }), + }); + }, + [detailName, navigateToApp, search] + ); return ( - <EuiLink - href={getCaseDetailsUrl({ id: detailName, search })} + <LinkAnchor + onClick={goToCaseDetails} + href={formatUrl(getCaseDetailsUrl({ id: detailName }))} data-test-subj="case-details-link" aria-label={i18n.CASE_DETAILS_LINK_ARIA(title ?? detailName)} > {children ? children : detailName} - </EuiLink> + </LinkAnchor> ); }; export const CaseDetailsLink = React.memo(CaseDetailsLinkComponent); CaseDetailsLink.displayName = 'CaseDetailsLink'; export const CreateCaseLink = React.memo<{ children: React.ReactNode }>(({ children }) => { - const search = useGetUrlSearch(navTabs.case); - return <EuiLink href={getCreateCaseUrl(search)}>{children}</EuiLink>; + const { formatUrl, search } = useFormatUrl(SecurityPageName.case); + const { navigateToApp } = useKibana().services.application; + const goToCreateCase = useCallback( + (ev) => { + ev.preventDefault(); + navigateToApp(`${APP_ID}:${SecurityPageName.case}`, { + path: getCreateCaseUrl(search), + }); + }, + [navigateToApp, search] + ); + return ( + <LinkAnchor onClick={goToCreateCase} href={formatUrl(getCreateCaseUrl())}> + {children} + </LinkAnchor> + ); }); CreateCaseLink.displayName = 'CreateCaseLink'; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/conditional_links/ml_host_conditional_container.tsx b/x-pack/plugins/security_solution/public/common/components/ml/conditional_links/ml_host_conditional_container.tsx index 6ca723c50c6817..0f3e0f9171e6dd 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/conditional_links/ml_host_conditional_container.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/conditional_links/ml_host_conditional_container.tsx @@ -11,7 +11,6 @@ import { Redirect, Route, Switch, RouteComponentProps } from 'react-router-dom'; import { addEntitiesToKql } from './add_entities_to_kql'; import { replaceKQLParts } from './replace_kql_parts'; import { emptyEntity, multipleEntities, getMultipleEntities } from './entity_helpers'; -import { SiemPageName } from '../../../../app/types'; import { HostsTableType } from '../../../../hosts/store/model'; import { url as urlUtils } from '../../../../../../../../src/plugins/kibana_utils/public'; @@ -42,7 +41,7 @@ export const MlHostConditionalContainer = React.memo<MlHostConditionalProps>(({ sort: false, encode: false, }); - return <Redirect to={`/${SiemPageName.hosts}?${reEncoded}`} />; + return <Redirect to={`?${reEncoded}`} />; }} /> <Route @@ -66,9 +65,7 @@ export const MlHostConditionalContainer = React.memo<MlHostConditionalProps>(({ encode: false, }); - return ( - <Redirect to={`/${SiemPageName.hosts}/${HostsTableType.anomalies}?${reEncoded}`} /> - ); + return <Redirect to={`/${HostsTableType.anomalies}?${reEncoded}`} />; } else if (multipleEntities(hostName)) { const hosts: string[] = getMultipleEntities(hostName); queryStringDecoded.query = addEntitiesToKql( @@ -81,20 +78,14 @@ export const MlHostConditionalContainer = React.memo<MlHostConditionalProps>(({ encode: false, }); - return ( - <Redirect to={`/${SiemPageName.hosts}/${HostsTableType.anomalies}?${reEncoded}`} /> - ); + return <Redirect to={`/${HostsTableType.anomalies}?${reEncoded}`} />; } else { const reEncoded = stringify(urlUtils.encodeQuery(queryStringDecoded), { sort: false, encode: false, }); - return ( - <Redirect - to={`/${SiemPageName.hosts}/${hostName}/${HostsTableType.anomalies}?${reEncoded}`} - /> - ); + return <Redirect to={`/${hostName}/${HostsTableType.anomalies}?${reEncoded}`} />; } }} /> diff --git a/x-pack/plugins/security_solution/public/common/components/ml/conditional_links/ml_network_conditional_container.tsx b/x-pack/plugins/security_solution/public/common/components/ml/conditional_links/ml_network_conditional_container.tsx index 05049cd9b4ea5e..60242276dcad7f 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/conditional_links/ml_network_conditional_container.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/conditional_links/ml_network_conditional_container.tsx @@ -11,7 +11,6 @@ import { Redirect, Route, Switch, RouteComponentProps } from 'react-router-dom'; import { addEntitiesToKql } from './add_entities_to_kql'; import { replaceKQLParts } from './replace_kql_parts'; import { emptyEntity, getMultipleEntities, multipleEntities } from './entity_helpers'; -import { SiemPageName } from '../../../../app/types'; import { url as urlUtils } from '../../../../../../../../src/plugins/kibana_utils/public'; @@ -43,7 +42,7 @@ export const MlNetworkConditionalContainer = React.memo<MlNetworkConditionalProp encode: false, }); - return <Redirect to={`/${SiemPageName.network}?${reEncoded}`} />; + return <Redirect to={`?${reEncoded}`} />; }} /> <Route @@ -68,7 +67,7 @@ export const MlNetworkConditionalContainer = React.memo<MlNetworkConditionalProp encode: false, }); - return <Redirect to={`/${SiemPageName.network}?${reEncoded}`} />; + return <Redirect to={`?${reEncoded}`} />; } else if (multipleEntities(ip)) { const ips: string[] = getMultipleEntities(ip); queryStringDecoded.query = addEntitiesToKql( @@ -80,13 +79,13 @@ export const MlNetworkConditionalContainer = React.memo<MlNetworkConditionalProp sort: false, encode: false, }); - return <Redirect to={`/${SiemPageName.network}?${reEncoded}`} />; + return <Redirect to={`?${reEncoded}`} />; } else { const reEncoded = stringify(urlUtils.encodeQuery(queryStringDecoded), { sort: false, encode: false, }); - return <Redirect to={`/${SiemPageName.network}/ip/${ip}?${reEncoded}`} />; + return <Redirect to={`/ip/${ip}?${reEncoded}`} />; } }} /> diff --git a/x-pack/plugins/security_solution/public/common/components/ml/links/create_explorer_link.test.ts b/x-pack/plugins/security_solution/public/common/components/ml/links/create_explorer_link.test.ts index 387b0f1001881d..4a25f82a94a618 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/links/create_explorer_link.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/links/create_explorer_link.test.ts @@ -22,7 +22,7 @@ describe('create_explorer_link', () => { new Date('3000').valueOf() ); expect(entities).toEqual( - "ml#/explorer?_g=(ml:(jobIds:!(job-1)),refreshInterval:(display:Off,pause:!f,value:0),time:(from:'1970-01-01T00:00:00.000Z',mode:absolute,to:'3000-01-01T00:00:00.000Z'))&_a=(mlExplorerFilter:(),mlExplorerSwimlane:(),mlSelectLimit:(display:'10',val:10),mlShowCharts:!t)" + "#/explorer?_g=(ml:(jobIds:!(job-1)),refreshInterval:(display:Off,pause:!f,value:0),time:(from:'1970-01-01T00:00:00.000Z',mode:absolute,to:'3000-01-01T00:00:00.000Z'))&_a=(mlExplorerFilter:(),mlExplorerSwimlane:(),mlSelectLimit:(display:'10',val:10),mlShowCharts:!t)" ); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/links/create_explorer_link.ts b/x-pack/plugins/security_solution/public/common/components/ml/links/create_explorer_link.tsx similarity index 55% rename from x-pack/plugins/security_solution/public/common/components/ml/links/create_explorer_link.ts rename to x-pack/plugins/security_solution/public/common/components/ml/links/create_explorer_link.tsx index 4e3efeb05e414b..e00f53a08a9184 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/links/create_explorer_link.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/links/create_explorer_link.tsx @@ -4,13 +4,42 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiLink } from '@elastic/eui'; +import React from 'react'; import { Anomaly } from '../types'; +import { useKibana } from '../../../lib/kibana'; + +interface ExplorerLinkProps { + score: Anomaly; + startDate: number; + endDate: number; + linkName: React.ReactNode; +} + +export const ExplorerLink: React.FC<ExplorerLinkProps> = ({ + score, + startDate, + endDate, + linkName, +}) => { + const { getUrlForApp } = useKibana().services.application; + return ( + <EuiLink + href={`${getUrlForApp('ml', { + path: createExplorerLink(score, startDate, endDate), + })}`} + target="_blank" + > + {linkName} + </EuiLink> + ); +}; export const createExplorerLink = (score: Anomaly, startDate: number, endDate: number): string => { const startDateIso = new Date(startDate).toISOString(); const endDateIso = new Date(endDate).toISOString(); - const JOB_PREFIX = `ml#/explorer?_g=(ml:(jobIds:!(${score.jobId}))`; + const JOB_PREFIX = `#/explorer?_g=(ml:(jobIds:!(${score.jobId}))`; const REFRESH_INTERVAL = `,refreshInterval:(display:Off,pause:!f,value:0),time:(from:'${startDateIso}',mode:absolute,to:'${endDateIso}'))`; const INTERVAL_SELECTION = `&_a=(mlExplorerFilter:(),mlExplorerSwimlane:(),mlSelectLimit:(display:'10',val:10),mlShowCharts:!t)`; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap index 35934efa558be3..a9ec474a7b6840 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap @@ -125,12 +125,77 @@ exports[`anomaly_scores renders correctly against snapshot 1`] = ` <EuiFlexItem grow={false} > - <EuiLink - href="ml#/explorer?_g=(ml:(jobIds:!(job-1)),refreshInterval:(display:Off,pause:!f,value:0),time:(from:'1970-01-01T00:00:00.000Z',mode:absolute,to:'3000-01-01T00:00:00.000Z'))&_a=(mlExplorerFilter:(),mlExplorerSwimlane:(),mlSelectLimit:(display:'10',val:10),mlShowCharts:!t)" - target="_blank" - > - View in Machine Learning - </EuiLink> + <ExplorerLink + endDate={32503680000000} + linkName="View in Machine Learning" + score={ + Object { + "detectorIndex": 0, + "entityName": "process.name", + "entityValue": "du", + "influencers": Array [ + Object { + "host.name": "zeek-iowa", + }, + Object { + "process.name": "du", + }, + Object { + "user.name": "root", + }, + ], + "jobId": "job-1", + "rowId": "1561157194802_0", + "severity": 16.193669439507826, + "source": Object { + "actual": Array [ + 1, + ], + "bucket_span": 900, + "by_field_name": "process.name", + "by_field_value": "du", + "detector_index": 0, + "function": "rare", + "function_description": "rare", + "influencers": Array [ + Object { + "influencer_field_name": "user.name", + "influencer_field_values": Array [ + "root", + ], + }, + Object { + "influencer_field_name": "process.name", + "influencer_field_values": Array [ + "du", + ], + }, + Object { + "influencer_field_name": "host.name", + "influencer_field_values": Array [ + "zeek-iowa", + ], + }, + ], + "initial_record_score": 16.193669439507826, + "is_interim": false, + "job_id": "job-1", + "multi_bucket_impact": 0, + "partition_field_name": "host.name", + "partition_field_value": "zeek-iowa", + "probability": 0.024041164411288146, + "record_score": 16.193669439507826, + "result_type": "record", + "timestamp": 1560664800000, + "typical": Array [ + 0.024041164411288146, + ], + }, + "time": 1560664800000, + } + } + startDate={0} + /> </EuiFlexItem> </EuiFlexGroup>, "title": <React.Fragment> diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/create_descriptions_list.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/create_descriptions_list.test.tsx.snap index 0a136eb31fd6d3..2e771f9f045b88 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/create_descriptions_list.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/create_descriptions_list.test.tsx.snap @@ -43,12 +43,77 @@ exports[`create_description_list renders correctly against snapshot 1`] = ` <EuiFlexItem grow={false} > - <EuiLink - href="ml#/explorer?_g=(ml:(jobIds:!(job-1)),refreshInterval:(display:Off,pause:!f,value:0),time:(from:'1970-01-01T00:00:00.000Z',mode:absolute,to:'3000-01-01T00:00:00.000Z'))&_a=(mlExplorerFilter:(),mlExplorerSwimlane:(),mlSelectLimit:(display:'10',val:10),mlShowCharts:!t)" - target="_blank" - > - View in Machine Learning - </EuiLink> + <ExplorerLink + endDate={32503680000000} + linkName="View in Machine Learning" + score={ + Object { + "detectorIndex": 0, + "entityName": "process.name", + "entityValue": "du", + "influencers": Array [ + Object { + "host.name": "zeek-iowa", + }, + Object { + "process.name": "du", + }, + Object { + "user.name": "root", + }, + ], + "jobId": "job-1", + "rowId": "1561157194802_0", + "severity": 16.193669439507826, + "source": Object { + "actual": Array [ + 1, + ], + "bucket_span": 900, + "by_field_name": "process.name", + "by_field_value": "du", + "detector_index": 0, + "function": "rare", + "function_description": "rare", + "influencers": Array [ + Object { + "influencer_field_name": "user.name", + "influencer_field_values": Array [ + "root", + ], + }, + Object { + "influencer_field_name": "process.name", + "influencer_field_values": Array [ + "du", + ], + }, + Object { + "influencer_field_name": "host.name", + "influencer_field_values": Array [ + "zeek-iowa", + ], + }, + ], + "initial_record_score": 16.193669439507826, + "is_interim": false, + "job_id": "job-1", + "multi_bucket_impact": 0, + "partition_field_name": "host.name", + "partition_field_value": "zeek-iowa", + "probability": 0.024041164411288146, + "record_score": 16.193669439507826, + "result_type": "record", + "timestamp": 1560664800000, + "typical": Array [ + 0.024041164411288146, + ], + }, + "time": 1560664800000, + } + } + startDate={0} + /> </EuiFlexItem> </EuiFlexGroup> </EuiDescriptionListDescription> diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/create_description_list.tsx b/x-pack/plugins/security_solution/public/common/components/ml/score/create_description_list.tsx index 0651bc5874860b..133f2407c45ed4 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/score/create_description_list.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/score/create_description_list.tsx @@ -14,7 +14,7 @@ import { getScoreString } from './score_health'; import { PreferenceFormattedDate } from '../../formatted_date'; import { createInfluencers } from './../influencers/create_influencers'; import * as i18n from './translations'; -import { createExplorerLink } from '../links/create_explorer_link'; +import { ExplorerLink } from '../links/create_explorer_link'; const LargeScore = styled(EuiText)` font-size: 45px; @@ -51,9 +51,12 @@ export const createDescriptionList = ( <EuiFlexGroup direction="column" gutterSize="none" responsive={false}> <EuiFlexItem grow={false}>{score.jobId}</EuiFlexItem> <EuiFlexItem grow={false}> - <EuiLink href={createExplorerLink(score, startDate, endDate)} target="_blank"> - {i18n.VIEW_IN_MACHINE_LEARNING} - </EuiLink> + <ExplorerLink + score={score} + startDate={startDate} + endDate={endDate} + linkName={i18n.VIEW_IN_MACHINE_LEARNING} + /> </EuiFlexItem> </EuiFlexGroup> ), diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.tsx index 53da31af4ad673..fc89189bf4f465 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.tsx @@ -7,7 +7,7 @@ /* eslint-disable react/display-name */ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { Columns } from '../../paginated_table'; import { AnomaliesByHost, Anomaly, NarrowDateRange } from '../types'; import { getRowItemDraggable } from '../../tables/helpers'; @@ -18,7 +18,7 @@ import { HostDetailsLink } from '../../links'; import * as i18n from './translations'; import { getEntries } from '../get_entries'; import { DraggableScore } from '../score/draggable_score'; -import { createExplorerLink } from '../links/create_explorer_link'; +import { ExplorerLink } from '../links/create_explorer_link'; import { HostsType } from '../../../../hosts/store/model'; import { escapeDataProviderId } from '../../drag_and_drop/helpers'; import { FormattedRelativePreferenceDate } from '../../formatted_date'; @@ -55,12 +55,12 @@ export const getAnomaliesHostTableColumns = ( field: 'anomaly.jobId', sortable: true, render: (jobId, anomaliesByHost) => ( - <EuiLink - href={`${createExplorerLink(anomaliesByHost.anomaly, startDate, endDate)}`} - target="_blank" - > - {jobId} - </EuiLink> + <ExplorerLink + score={anomaliesByHost.anomaly} + startDate={startDate} + endDate={endDate} + linkName={jobId} + /> ), }, { diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx index abd4eec8898e44..ce4269afbe5b2a 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx @@ -7,7 +7,7 @@ /* eslint-disable react/display-name */ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { Columns } from '../../paginated_table'; import { Anomaly, AnomaliesByNetwork } from '../types'; @@ -19,7 +19,7 @@ import { IPDetailsLink } from '../../links'; import * as i18n from './translations'; import { getEntries } from '../get_entries'; import { DraggableScore } from '../score/draggable_score'; -import { createExplorerLink } from '../links/create_explorer_link'; +import { ExplorerLink } from '../links/create_explorer_link'; import { FormattedRelativePreferenceDate } from '../../formatted_date'; import { NetworkType } from '../../../../network/store/model'; import { escapeDataProviderId } from '../../drag_and_drop/helpers'; @@ -54,12 +54,12 @@ export const getAnomaliesNetworkTableColumns = ( field: 'anomaly.jobId', sortable: true, render: (jobId, anomaliesByHost) => ( - <EuiLink - href={`${createExplorerLink(anomaliesByHost.anomaly, startDate, endDate)}`} - target="_blank" - > - {jobId} - </EuiLink> + <ExplorerLink + score={anomaliesByHost.anomaly} + startDate={startDate} + endDate={endDate} + linkName={jobId} + /> ), }, { diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/__mocks__/use_get_url_search.ts b/x-pack/plugins/security_solution/public/common/components/navigation/__mocks__/use_get_url_search.ts new file mode 100644 index 00000000000000..d8a1db0aae70d1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/navigation/__mocks__/use_get_url_search.ts @@ -0,0 +1,9 @@ +/* + * 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 { SearchNavTab } from '../types'; + +export const useGetUrlSearch = (tab: SearchNavTab) => ''; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts index 8f1318aea37636..2c30f9a2e4ac32 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts @@ -38,28 +38,28 @@ const getMockObject = ( navTabs: { hosts: { disabled: false, - href: '#/link-to/hosts', + href: '/app/security/hosts', id: 'hosts', name: 'Hosts', urlKey: 'host', }, network: { disabled: false, - href: '#/link-to/network', + href: '/app/security/network', id: 'network', name: 'Network', urlKey: 'network', }, overview: { disabled: false, - href: '#/link-to/overview', + href: '/app/security/overview', id: 'overview', name: 'Overview', urlKey: 'overview', }, timelines: { disabled: false, - href: '#/link-to/timelines', + href: '/app/security/timelines', id: 'timelines', name: 'Timelines', urlKey: 'timeline', @@ -99,6 +99,9 @@ const getMockObject = ( }, }); +const getUrlForAppMock = (appId: string, options?: { path?: string; absolute?: boolean }) => + `${appId}${options?.path ?? ''}`; + describe('Navigation Breadcrumbs', () => { const hostName = 'siem-kibana'; @@ -108,15 +111,18 @@ describe('Navigation Breadcrumbs', () => { describe('getBreadcrumbsForRoute', () => { test('should return Host breadcrumbs when supplied host pathname', () => { - const breadcrumbs = getBreadcrumbsForRoute(getMockObject('hosts', '/hosts', undefined)); + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('hosts', '/', undefined), + getUrlForAppMock + ); expect(breadcrumbs).toEqual([ { - href: '#/link-to/overview', + href: '/app/security/overview', text: 'Security', }, { 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)))', + 'securitySolution: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', }, { @@ -127,13 +133,16 @@ describe('Navigation Breadcrumbs', () => { }); test('should return Network breadcrumbs when supplied network pathname', () => { - const breadcrumbs = getBreadcrumbsForRoute(getMockObject('network', '/network', undefined)); + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('network', '/', undefined), + getUrlForAppMock + ); expect(breadcrumbs).toEqual([ - { text: 'Security', href: '#/link-to/overview' }, + { text: 'Security', href: '/app/security/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)))', + 'securitySolution: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: 'Flows', @@ -144,61 +153,75 @@ describe('Navigation Breadcrumbs', () => { test('should return Timelines breadcrumbs when supplied timelines pathname', () => { const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('timelines', '/timelines', undefined) + getMockObject('timelines', '/', undefined), + getUrlForAppMock ); expect(breadcrumbs).toEqual([ - { text: 'Security', href: '#/link-to/overview' }, - { text: 'Timelines', href: '#/link-to/timelines' }, + { text: 'Security', href: '/app/security/overview' }, + { + text: 'Timelines', + href: + 'securitySolution:timelines?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 Host Details breadcrumbs when supplied a pathname with hostName', () => { - const breadcrumbs = getBreadcrumbsForRoute(getMockObject('hosts', '/hosts', hostName)); + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('hosts', '/', hostName), + getUrlForAppMock + ); expect(breadcrumbs).toEqual([ - { text: 'Security', href: '#/link-to/overview' }, + { text: 'Security', href: '/app/security/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)))', + 'securitySolution: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)))', + 'securitySolution: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 breadcrumbs = getBreadcrumbsForRoute(getMockObject('network', '/network', ipv4)); + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('network', '/', ipv4), + getUrlForAppMock + ); expect(breadcrumbs).toEqual([ - { text: 'Security', href: '#/link-to/overview' }, + { text: 'Security', href: '/app/security/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)))', + 'securitySolution: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: ipv4, - href: `#/link-to/network/ip/${ipv4}/source?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)))`, + href: `securitySolution:network/ip/${ipv4}/source?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: 'Flows', href: '' }, ]); }); test('should return IP Details breadcrumbs when supplied pathname with ipv6', () => { - const breadcrumbs = getBreadcrumbsForRoute(getMockObject('network', '/network', ipv6Encoded)); + const breadcrumbs = getBreadcrumbsForRoute( + getMockObject('network', '/', ipv6Encoded), + getUrlForAppMock + ); expect(breadcrumbs).toEqual([ - { text: 'Security', href: '#/link-to/overview' }, + { text: 'Security', href: '/app/security/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)))', + 'securitySolution: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: ipv6, - href: `#/link-to/network/ip/${ipv6Encoded}/source?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)))`, + href: `securitySolution:network/ip/${ipv6Encoded}/source?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: 'Flows', href: '' }, ]); @@ -207,18 +230,18 @@ describe('Navigation Breadcrumbs', () => { describe('setBreadcrumbs()', () => { test('should call chrome breadcrumb service with correct breadcrumbs', () => { - setBreadcrumbs(getMockObject('hosts', '/hosts', hostName), chromeMock); + setBreadcrumbs(getMockObject('hosts', '/', hostName), chromeMock, getUrlForAppMock); expect(setBreadcrumbsMock).toBeCalledWith([ - { text: 'Security', href: '#/link-to/overview' }, + { text: 'Security', href: '/app/security/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)))', + 'securitySolution: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)))', + 'securitySolution: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/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts index a202407b1270c2..5c1c68b802726f 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts @@ -15,24 +15,25 @@ import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../../network/p import { getBreadcrumbs as getCaseDetailsBreadcrumbs } from '../../../../cases/pages/utils'; import { getBreadcrumbs as getDetectionRulesBreadcrumbs } from '../../../../alerts/pages/detection_engine/rules/utils'; import { getBreadcrumbs as getTimelinesBreadcrumbs } from '../../../../timelines/pages'; -import { SiemPageName } from '../../../../app/types'; +import { SecurityPageName } from '../../../../app/types'; import { RouteSpyState, HostRouteSpyState, NetworkRouteSpyState, TimelineRouteSpyState, } from '../../../utils/route/types'; -import { getOverviewUrl } from '../../link_to'; +import { getAppOverviewUrl } from '../../link_to'; import { TabNavigationProps } from '../tab_navigation/types'; import { getSearch } from '../helpers'; -import { SearchNavTab } from '../types'; +import { GetUrlForApp, SearchNavTab } from '../types'; export const setBreadcrumbs = ( spyState: RouteSpyState & TabNavigationProps, - chrome: StartServices['chrome'] + chrome: StartServices['chrome'], + getUrlForApp: GetUrlForApp ) => { - const breadcrumbs = getBreadcrumbsForRoute(spyState); + const breadcrumbs = getBreadcrumbsForRoute(spyState, getUrlForApp); if (breadcrumbs) { chrome.setBreadcrumbs(breadcrumbs); } @@ -41,27 +42,28 @@ export const setBreadcrumbs = ( export const siemRootBreadcrumb: ChromeBreadcrumb[] = [ { text: APP_NAME, - href: getOverviewUrl(), + href: getAppOverviewUrl(), }, ]; const isNetworkRoutes = (spyState: RouteSpyState): spyState is NetworkRouteSpyState => - spyState != null && spyState.pageName === SiemPageName.network; + spyState != null && spyState.pageName === SecurityPageName.network; const isHostsRoutes = (spyState: RouteSpyState): spyState is HostRouteSpyState => - spyState != null && spyState.pageName === SiemPageName.hosts; + spyState != null && spyState.pageName === SecurityPageName.hosts; const isTimelinesRoutes = (spyState: RouteSpyState): spyState is TimelineRouteSpyState => - spyState != null && spyState.pageName === SiemPageName.timelines; + spyState != null && spyState.pageName === SecurityPageName.timelines; const isCaseRoutes = (spyState: RouteSpyState): spyState is RouteSpyState => - spyState != null && spyState.pageName === SiemPageName.case; + spyState != null && spyState.pageName === SecurityPageName.case; -const isDetectionsRoutes = (spyState: RouteSpyState) => - spyState != null && spyState.pageName === SiemPageName.detections; +const isAlertsRoutes = (spyState: RouteSpyState) => + spyState != null && spyState.pageName === SecurityPageName.alerts; export const getBreadcrumbsForRoute = ( - object: RouteSpyState & TabNavigationProps + object: RouteSpyState & TabNavigationProps, + getUrlForApp: GetUrlForApp ): ChromeBreadcrumb[] | null => { const spyState: RouteSpyState = omit('navTabs', object); if (isHostsRoutes(spyState) && object.navTabs) { @@ -77,7 +79,8 @@ export const getBreadcrumbsForRoute = ( urlStateKeys.reduce( (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], [] - ) + ), + getUrlForApp ), ]; } @@ -94,12 +97,13 @@ export const getBreadcrumbsForRoute = ( urlStateKeys.reduce( (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], [] - ) + ), + getUrlForApp ), ]; } - if (isDetectionsRoutes(spyState) && object.navTabs) { - const tempNav: SearchNavTab = { urlKey: 'detections', isDetailPage: false }; + if (isAlertsRoutes(spyState) && object.navTabs) { + const tempNav: SearchNavTab = { urlKey: 'alerts', isDetailPage: false }; let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; if (spyState.tabName != null) { urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; @@ -112,7 +116,8 @@ export const getBreadcrumbsForRoute = ( urlStateKeys.reduce( (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], [] - ) + ), + getUrlForApp ), ]; } @@ -130,7 +135,8 @@ export const getBreadcrumbsForRoute = ( urlStateKeys.reduce( (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], [] - ) + ), + getUrlForApp ), ]; } @@ -148,7 +154,8 @@ export const getBreadcrumbsForRoute = ( urlStateKeys.reduce( (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], [] - ) + ), + getUrlForApp ), ]; } diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx index fd96885e5bc103..a99497090f8435 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx @@ -15,14 +15,40 @@ import { HostsTableType } from '../../../hosts/store/model'; import { RouteSpyState } from '../../utils/route/types'; import { SiemNavigationProps, SiemNavigationComponentProps } from './types'; +jest.mock('react-router-dom', () => { + const original = jest.requireActual('react-router-dom'); + + return { + ...original, + useHistory: () => ({ + useHistory: jest.fn(), + }), + }; +}); + jest.mock('./breadcrumbs', () => ({ setBreadcrumbs: jest.fn(), })); +const mockGetUrlForApp = jest.fn(); +jest.mock('../../lib/kibana', () => { + return { + useKibana: () => ({ + services: { + chrome: undefined, + application: { + navigateToApp: jest.fn(), + getUrlForApp: mockGetUrlForApp, + }, + }, + }), + }; +}); +jest.mock('../link_to'); describe('SIEM Navigation', () => { const mockProps: SiemNavigationComponentProps & SiemNavigationProps & RouteSpyState = { pageName: 'hosts', - pathName: '/hosts', + pathName: '/', detailName: undefined, search: '', tabName: HostsTableType.authentications, @@ -65,63 +91,64 @@ describe('SIEM Navigation', () => { { detailName: undefined, navTabs: { + alerts: { + disabled: false, + href: '/app/security/alerts', + id: 'alerts', + name: 'Alerts', + urlKey: 'alerts', + }, case: { disabled: false, - href: '#/link-to/case', + href: '/app/security/cases', id: 'case', name: 'Cases', urlKey: 'case', }, management: { disabled: false, - href: '#/management', + href: '/app/security/management', id: 'management', name: 'Management', urlKey: 'management', }, - detections: { - disabled: false, - href: '#/link-to/detections', - id: 'detections', - name: 'Detections', - urlKey: 'detections', - }, hosts: { disabled: false, - href: '#/link-to/hosts', + href: '/app/security/hosts', id: 'hosts', name: 'Hosts', urlKey: 'host', }, network: { disabled: false, - href: '#/link-to/network', + href: '/app/security/network', id: 'network', name: 'Network', urlKey: 'network', }, overview: { disabled: false, - href: '#/link-to/overview', + href: '/app/security/overview', id: 'overview', name: 'Overview', urlKey: 'overview', }, timelines: { disabled: false, - href: '#/link-to/timelines', + href: '/app/security/timelines', id: 'timelines', name: 'Timelines', urlKey: 'timeline', }, }, pageName: 'hosts', - pathName: '/hosts', + pathName: '/', search: '', state: undefined, tabName: 'authentications', query: { query: '', language: 'kuery' }, filters: [], + flowTarget: undefined, savedQuery: undefined, timeline: { id: '', @@ -150,79 +177,82 @@ describe('SIEM Navigation', () => { }, }, }, - undefined + undefined, + mockGetUrlForApp ); }); test('it calls setBreadcrumbs with correct path on update', () => { wrapper.setProps({ pageName: 'network', - pathName: '/network', + pathName: '/', tabName: undefined, }); wrapper.update(); expect(setBreadcrumbs).toHaveBeenNthCalledWith( - 1, + 2, { detailName: undefined, filters: [], + flowTarget: undefined, navTabs: { + alerts: { + disabled: false, + href: '/app/security/alerts', + id: 'alerts', + name: 'Alerts', + urlKey: 'alerts', + }, case: { disabled: false, - href: '#/link-to/case', + href: '/app/security/cases', id: 'case', name: 'Cases', urlKey: 'case', }, - detections: { - disabled: false, - href: '#/link-to/detections', - id: 'detections', - name: 'Detections', - urlKey: 'detections', - }, + hosts: { disabled: false, - href: '#/link-to/hosts', + href: '/app/security/hosts', id: 'hosts', name: 'Hosts', urlKey: 'host', }, management: { disabled: false, - href: '#/management', + href: '/app/security/management', id: 'management', name: 'Management', urlKey: 'management', }, network: { disabled: false, - href: '#/link-to/network', + href: '/app/security/network', id: 'network', name: 'Network', urlKey: 'network', }, overview: { disabled: false, - href: '#/link-to/overview', + href: '/app/security/overview', id: 'overview', name: 'Overview', urlKey: 'overview', }, timelines: { disabled: false, - href: '#/link-to/timelines', + href: '/app/security/timelines', id: 'timelines', name: 'Timelines', urlKey: 'timeline', }, }, - pageName: 'hosts', - pathName: '/hosts', + pageName: 'network', + pathName: '/', query: { language: 'kuery', query: '' }, savedQuery: undefined, search: '', state: undefined, - tabName: 'authentications', + tabName: undefined, timeline: { id: '', isOpen: false }, timerange: { global: { @@ -247,7 +277,8 @@ describe('SIEM Navigation', () => { }, }, }, - undefined + undefined, + mockGetUrlForApp ); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx index 0cbff9e70eff76..5ee35e7da0f3ee 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx @@ -31,10 +31,13 @@ export const SiemNavigationComponent: React.FC< flowTarget, state, }) => { - const { chrome } = useKibana().services; + const { + chrome, + application: { getUrlForApp }, + } = useKibana().services; useEffect(() => { - if (pathName) { + if (pathName || pageName) { setBreadcrumbs( { query: urlState.query, @@ -51,11 +54,12 @@ export const SiemNavigationComponent: React.FC< timeline: urlState.timeline, state, }, - chrome + chrome, + getUrlForApp ); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [chrome, pathName, search, navTabs, urlState, state]); + }, [chrome, pageName, pathName, search, navTabs, urlState, state]); return ( <TabNavigation diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx index b9572caece94fa..977c7808b6c86e 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx @@ -8,7 +8,7 @@ import { mount } from 'enzyme'; import React from 'react'; import { navTabs } from '../../../../app/home/home_navigations'; -import { SiemPageName } from '../../../../app/types'; +import { SecurityPageName } from '../../../../app/types'; import { navTabsHostDetails } from '../../../../hosts/pages/details/nav_tabs'; import { HostsTableType } from '../../../../hosts/store/model'; import { RouteSpyState } from '../../../utils/route/types'; @@ -16,8 +16,22 @@ import { CONSTANTS } from '../../url_state/constants'; import { TabNavigationComponent } from './'; import { TabNavigationProps } from './types'; +jest.mock('../../../lib/kibana'); +jest.mock('../../link_to'); + +jest.mock('react-router-dom', () => { + const original = jest.requireActual('react-router-dom'); + + return { + ...original, + useHistory: () => ({ + push: jest.fn(), + }), + }; +}); + describe('Tab Navigation', () => { - const pageName = SiemPageName.hosts; + const pageName = SecurityPageName.hosts; const hostName = 'siem-window'; const tabName = HostsTableType.authentications; const pathName = `/${pageName}/${hostName}/${tabName}`; @@ -76,13 +90,6 @@ describe('Tab Navigation', () => { wrapper.update(); expect(networkTab().prop('isSelected')).toBeTruthy(); }); - test('it carries the url state in the link', () => { - const wrapper = mount(<TabNavigationComponent {...mockProps} />); - const firstTab = wrapper.find('EuiTab[data-test-subj="navigation-network"]'); - expect(firstTab.props().href).toBe( - "#/link-to/network?query=(language:kuery,query:'host.name:%22siem-es%22')&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('Table Navigation', () => { @@ -137,8 +144,8 @@ describe('Tab Navigation', () => { wrapper.find(`[data-test-subj="navigation-${HostsTableType.events}"]`).first(); expect(tableNavigationTab().prop('isSelected')).toBeFalsy(); wrapper.setProps({ - pageName: SiemPageName.hosts, - pathName: `/${SiemPageName.hosts}`, + pageName: SecurityPageName.hosts, + pathName: `/${SecurityPageName.hosts}`, tabName: HostsTableType.events, }); wrapper.update(); @@ -149,9 +156,7 @@ describe('Tab Navigation', () => { const firstTab = wrapper.find( `EuiTab[data-test-subj="navigation-${HostsTableType.authentications}"]` ); - expect(firstTab.props().href).toBe( - `#/${pageName}/${hostName}/${HostsTableType.authentications}?query=(language:kuery,query:'host.name:%22siem-es%22')&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)))` - ); + expect(firstTab.props().href).toBe('/siem-window/authentications'); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.tsx index 15dff1ca82d811..217ad0e58570f5 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.tsx @@ -6,30 +6,54 @@ import { EuiTab, EuiTabs } from '@elastic/eui'; import { getOr } from 'lodash/fp'; import React, { useEffect, useState, useCallback, useMemo } from 'react'; +import { useHistory } from 'react-router-dom'; +import { APP_ID } from '../../../../../common/constants'; import { track, METRIC_TYPE, TELEMETRY_EVENT } from '../../../lib/telemetry'; import { getSearch } from '../helpers'; import { TabNavigationProps, TabNavigationItemProps } from './types'; +import { useKibana } from '../../../lib/kibana'; +import { SecurityPageName } from '../../../../app/types'; +import { useFormatUrl } from '../../link_to'; const TabNavigationItemComponent = ({ + disabled, href, hrefWithSearch, id, - disabled, name, isSelected, + pageId, + urlSearch, }: TabNavigationItemProps) => { - const handleClick = useCallback(() => { - track(METRIC_TYPE.CLICK, `${TELEMETRY_EVENT.TAB_CLICKED}${id}`); - }, [id]); - + const history = useHistory(); + const { navigateToApp, getUrlForApp } = useKibana().services.application; + const { formatUrl } = useFormatUrl(((pageId ?? id) as unknown) as SecurityPageName); + const handleClick = useCallback( + (ev) => { + ev.preventDefault(); + if (id in SecurityPageName && pageId == null) { + navigateToApp(`${APP_ID}:${id}`, { path: urlSearch }); + } else { + history.push(hrefWithSearch); + } + track(METRIC_TYPE.CLICK, `${TELEMETRY_EVENT.TAB_CLICKED}${id}`); + }, + [history, hrefWithSearch, id, navigateToApp, pageId, urlSearch] + ); + const appHref = + pageId != null + ? formatUrl(href) + : getUrlForApp(`${APP_ID}:${id}`, { + path: urlSearch, + }); return ( <EuiTab - data-href={href} + data-href={appHref} data-test-subj={`navigation-${id}`} disabled={disabled} - href={hrefWithSearch} isSelected={isSelected} + href={appHref} onClick={handleClick} > {name} @@ -47,7 +71,11 @@ export const TabNavigationComponent = (props: TabNavigationProps) => { getOr( '', 'id', - Object.values(navTabs).find((item) => tabName === item.id || pageName === item.id) + Object.values(navTabs).find( + (item) => + (tabName === item.id && item.pageId != null) || + (pageName === item.id && item.pageId == null) + ) ), [pageName, tabName, navTabs] ); @@ -67,6 +95,7 @@ export const TabNavigationComponent = (props: TabNavigationProps) => { Object.values(navTabs).map((tab) => { const isSelected = selectedTabId === tab.id; const { query, filters, savedQuery, timerange, timeline } = props; + const search = getSearch(tab, { query, filters, savedQuery, timerange, timeline }); const hrefWithSearch = tab.href + getSearch(tab, { query, filters, savedQuery, timerange, timeline }); @@ -75,10 +104,12 @@ export const TabNavigationComponent = (props: TabNavigationProps) => { key={`navigation-${tab.id}`} id={tab.id} href={tab.href} + hrefWithSearch={hrefWithSearch} name={tab.name} disabled={tab.disabled} - hrefWithSearch={hrefWithSearch} + pageId={tab.pageId} isSelected={isSelected} + urlSearch={search} /> ); }), diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/types.ts b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/types.ts index a283691cfe0df4..7f72e37c74d56e 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/types.ts @@ -30,4 +30,6 @@ export interface TabNavigationItemProps { disabled: boolean; name: string; isSelected: boolean; + urlSearch: string; + pageId?: string; } diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/types.ts b/x-pack/plugins/security_solution/public/common/components/navigation/types.ts index f0256813c29e78..80302be18355c4 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/types.ts @@ -9,6 +9,7 @@ import { HostsTableType } from '../../../hosts/store/model'; import { UrlInputsModel } from '../../store/inputs/model'; import { TimelineUrl } from '../../../timelines/store/timeline/model'; import { CONSTANTS, UrlStateType } from '../url_state/constants'; +import { SecurityPageName } from '../../../app/types'; export interface SiemNavigationProps { display?: 'default' | 'condensed'; @@ -37,4 +38,21 @@ export interface NavTab { disabled: boolean; urlKey: UrlStateType; isDetailPage?: boolean; + pageId?: SecurityPageName; } + +export type SiemNavTabKey = + | SecurityPageName.overview + | SecurityPageName.hosts + | SecurityPageName.network + | SecurityPageName.alerts + | SecurityPageName.timelines + | SecurityPageName.case + | SecurityPageName.management; + +export type SiemNavTab = Record<SiemNavTabKey, NavTab>; + +export type GetUrlForApp = ( + appId: string, + options?: { path?: string; absolute?: boolean } +) => string; diff --git a/x-pack/plugins/security_solution/public/common/components/news_feed/no_news/index.tsx b/x-pack/plugins/security_solution/public/common/components/news_feed/no_news/index.tsx index 8061a8f9799e32..d626433de1b63f 100644 --- a/x-pack/plugins/security_solution/public/common/components/news_feed/no_news/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/news_feed/no_news/index.tsx @@ -8,17 +8,21 @@ import { EuiLink, EuiText } from '@elastic/eui'; import React from 'react'; import * as i18n from '../translations'; +import { useBasePath } from '../../../lib/kibana'; -export const NoNews = React.memo(() => ( - <> - <EuiText color="subdued" size="s"> - {i18n.NO_NEWS_MESSAGE}{' '} - <EuiLink href={'/app/management/kibana/settings'}> - {i18n.ADVANCED_SETTINGS_LINK_TITLE} - </EuiLink> - {'.'} - </EuiText> - </> -)); +export const NoNews = React.memo(() => { + const basePath = useBasePath(); + return ( + <> + <EuiText color="subdued" size="s"> + {i18n.NO_NEWS_MESSAGE}{' '} + <EuiLink href={`${basePath}/app/management/kibana/settings`}> + {i18n.ADVANCED_SETTINGS_LINK_TITLE} + </EuiLink> + {'.'} + </EuiText> + </> + ); +}); NoNews.displayName = 'NoNews'; diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx index 1c24c325dc9ec5..ae25e66b2af866 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx @@ -26,6 +26,18 @@ import { timelineDefaults, } from '../../../timelines/components/manage_timeline'; +jest.mock('react-router-dom', () => { + const original = jest.requireActual('react-router-dom'); + + return { + ...original, + useHistory: () => ({ + useHistory: jest.fn(), + }), + }; +}); + +jest.mock('../link_to'); jest.mock('../../lib/kibana'); jest.mock('../../../timelines/store/timeline/actions'); diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx index 5e1476f62216be..b1979c501c7786 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx @@ -13,7 +13,19 @@ import { setAbsoluteRangeDatePicker } from '../../store/inputs/actions'; import { allEvents, defaultOptions } from './helpers'; import { TopN } from './top_n'; +jest.mock('react-router-dom', () => { + const original = jest.requireActual('react-router-dom'); + + return { + ...original, + useHistory: () => ({ + useHistory: jest.fn(), + }), + }; +}); + jest.mock('../../lib/kibana'); +jest.mock('../link_to'); jest.mock('uuid', () => { return { diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts b/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts index 1faff2594ce804..71faec88e85a02 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts @@ -8,7 +8,7 @@ export enum CONSTANTS { appQuery = 'query', caseDetails = 'case.details', casePage = 'case.page', - detectionsPage = 'detections.page', + alertsPage = 'alerts.page', filters = 'filters', hostsDetails = 'hosts.details', hostsPage = 'hosts.page', @@ -25,7 +25,7 @@ export enum CONSTANTS { export type UrlStateType = | 'case' - | 'detections' + | 'alerts' | 'host' | 'network' | 'overview' diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts index 8f13e4dd0cdcf7..c270a99d3c51e5 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts @@ -12,7 +12,7 @@ import * as H from 'history'; import { Query, Filter } from '../../../../../../../src/plugins/data/public'; import { url } from '../../../../../../../src/plugins/kibana_utils/public'; -import { SiemPageName } from '../../../app/types'; +import { SecurityPageName } from '../../../app/types'; import { inputsSelectors, State } from '../../store'; import { UrlInputsModel } from '../../store/inputs/model'; import { TimelineUrl } from '../../../timelines/store/timeline/model'; @@ -84,17 +84,17 @@ export const replaceQueryStringInLocation = ( }; export const getUrlType = (pageName: string): UrlStateType => { - if (pageName === SiemPageName.overview) { + if (pageName === SecurityPageName.overview) { return 'overview'; - } else if (pageName === SiemPageName.hosts) { + } else if (pageName === SecurityPageName.hosts) { return 'host'; - } else if (pageName === SiemPageName.network) { + } else if (pageName === SecurityPageName.network) { return 'network'; - } else if (pageName === SiemPageName.detections) { - return 'detections'; - } else if (pageName === SiemPageName.timelines) { + } else if (pageName === SecurityPageName.alerts) { + return 'alerts'; + } else if (pageName === SecurityPageName.timelines) { return 'timeline'; - } else if (pageName === SiemPageName.case) { + } else if (pageName === SecurityPageName.case) { return 'case'; } return 'overview'; @@ -114,19 +114,20 @@ export const makeMapStateToProps = () => { const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); const getGlobalSavedQuerySelector = inputsSelectors.globalSavedQuerySelector(); - const getTimelines = timelineSelectors.getTimelines(); + const getTimeline = timelineSelectors.getTimelineByIdSelector(); const mapStateToProps = (state: State) => { const inputState = getInputsSelector(state); const { linkTo: globalLinkTo, timerange: globalTimerange } = inputState.global; const { linkTo: timelineLinkTo, timerange: timelineTimerange } = inputState.timeline; - const timeline = Object.entries(getTimelines(state)).reduce( - (obj, [timelineId, timelineObj]) => ({ - id: timelineObj.savedObjectId != null ? timelineObj.savedObjectId : '', - isOpen: timelineObj.show, - }), - { id: '', isOpen: false } - ); + const flyoutTimeline = getTimeline(state, 'timeline-1'); + const timeline = + flyoutTimeline != null + ? { + id: flyoutTimeline.savedObjectId != null ? flyoutTimeline.savedObjectId : '', + isOpen: flyoutTimeline.show, + } + : { id: '', isOpen: false }; let searchAttr: { [CONSTANTS.appQuery]?: Query; diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx index 138ab08e894a53..20374affbdf89f 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx @@ -8,7 +8,7 @@ import { mount } from 'enzyme'; import React from 'react'; import { HookWrapper } from '../../mock'; -import { SiemPageName } from '../../../app/types'; +import { SecurityPageName } from '../../../app/types'; import { RouteSpyState } from '../../utils/route/types'; import { CONSTANTS } from './constants'; import { @@ -26,7 +26,7 @@ import { wait } from '../../lib/helpers'; let mockProps: UrlStateContainerPropTypes; const mockRouteSpy: RouteSpyState = { - pageName: SiemPageName.network, + pageName: SecurityPageName.network, detailName: undefined, tabName: undefined, search: '', @@ -186,14 +186,14 @@ describe('UrlStateContainer', () => { page: CONSTANTS.hostsPage, examplePath: '/hosts', namespaceLower: 'hosts', - pageName: SiemPageName.hosts, + pageName: SecurityPageName.hosts, detailName: undefined, }).relativeTimeSearch.undefinedQuery, }); wrapper.update(); await wait(); - if (CONSTANTS.detectionsPage === page) { + if (CONSTANTS.alertsPage === page) { expect(mockSetRelativeRangeDatePicker.mock.calls[3][0]).toEqual({ from: 11223344556677, fromStr: 'now-1d/d', diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx index 1650e3bea2022d..f7502661da308b 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx @@ -8,7 +8,7 @@ import { mount } from 'enzyme'; import React from 'react'; import { HookWrapper } from '../../mock/hook_wrapper'; -import { SiemPageName } from '../../../app/types'; +import { SecurityPageName } from '../../../app/types'; import { CONSTANTS } from './constants'; import { getFilterQuery, getMockPropsObj, mockHistory, testCases } from './test_dependencies'; @@ -42,7 +42,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => page: CONSTANTS.networkPage, examplePath: '/network', namespaceLower: 'network', - pageName: SiemPageName.network, + pageName: SecurityPageName.network, detailName: undefined, }).noSearch.definedQuery; const wrapper = mount( @@ -93,7 +93,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => page: CONSTANTS.networkPage, examplePath: '/network', namespaceLower: 'network', - pageName: SiemPageName.network, + pageName: SecurityPageName.network, detailName: undefined, }).noSearch.undefinedQuery; const wrapper = mount( @@ -124,7 +124,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => page: CONSTANTS.networkPage, examplePath: '/network', namespaceLower: 'network', - pageName: SiemPageName.network, + pageName: SecurityPageName.network, detailName: undefined, }).noSearch.undefinedQuery; @@ -187,14 +187,14 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => page: CONSTANTS.hostsPage, examplePath: '/hosts', namespaceLower: 'hosts', - pageName: SiemPageName.hosts, + pageName: SecurityPageName.hosts, detailName: undefined, }).noSearch.undefinedQuery; const updatedProps = getMockPropsObj({ page: CONSTANTS.networkPage, examplePath: '/network', namespaceLower: 'network', - pageName: SiemPageName.network, + pageName: SecurityPageName.network, detailName: undefined, }).noSearch.definedQuery; const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts b/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts index 0c69e24fb74761..dec1672b076eb5 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts @@ -7,7 +7,7 @@ import { ActionCreator } from 'typescript-fsa'; import { DispatchUpdateTimeline } from '../../../timelines/components/open_timeline/types'; import { navTabs } from '../../../app/home/home_navigations'; -import { SiemPageName } from '../../../app/types'; +import { SecurityPageName } from '../../../app/types'; import { inputsActions } from '../../store/actions'; import { CONSTANTS } from './constants'; @@ -71,7 +71,7 @@ export const mockHistory = { }; export const defaultProps: UrlStateContainerPropTypes = { - pageName: SiemPageName.network, + pageName: SecurityPageName.network, detailName: undefined, tabName: HostsTableType.authentications, search: '', @@ -292,7 +292,7 @@ export const testCases: Array<[ /* namespaceUpper */ 'Network', /* pathName */ '/network', /* type */ networkModel.NetworkType.page, - /* pageName */ SiemPageName.network, + /* pageName */ SecurityPageName.network, /* detailName */ undefined, ], [ @@ -301,7 +301,7 @@ export const testCases: Array<[ /* namespaceUpper */ 'Hosts', /* pathName */ '/hosts', /* type */ hostsModel.HostsType.page, - /* pageName */ SiemPageName.hosts, + /* pageName */ SecurityPageName.hosts, /* detailName */ undefined, ], [ @@ -310,7 +310,7 @@ export const testCases: Array<[ /* namespaceUpper */ 'Hosts', /* pathName */ '/hosts/siem-es', /* type */ hostsModel.HostsType.details, - /* pageName */ SiemPageName.hosts, + /* pageName */ SecurityPageName.hosts, /* detailName */ 'host-test', ], [ @@ -319,7 +319,7 @@ export const testCases: Array<[ /* namespaceUpper */ 'Network', /* pathName */ '/network/ip/100.90.80', /* type */ networkModel.NetworkType.details, - /* pageName */ SiemPageName.network, + /* pageName */ SecurityPageName.network, /* detailName */ '100.90.80', ], [ @@ -328,7 +328,7 @@ export const testCases: Array<[ /* namespaceUpper */ 'Overview', /* pathName */ '/overview', /* type */ null, - /* pageName */ SiemPageName.overview, + /* pageName */ SecurityPageName.overview, /* detailName */ undefined, ], [ @@ -337,7 +337,7 @@ export const testCases: Array<[ /* namespaceUpper */ 'Timeline', /* pathName */ '/timeline', /* type */ null, - /* pageName */ SiemPageName.timelines, + /* pageName */ SecurityPageName.timelines, /* detailName */ undefined, ], ]; diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/types.ts b/x-pack/plugins/security_solution/public/common/components/url_state/types.ts index 8881a82e5cd1c0..8ca43cb576d326 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/types.ts @@ -32,7 +32,7 @@ export const ALL_URL_STATE_KEYS: KeyUrlState[] = [ ]; export const URL_STATE_KEYS: Record<UrlStateType, KeyUrlState[]> = { - detections: [ + alerts: [ CONSTANTS.appQuery, CONSTANTS.filters, CONSTANTS.savedQuery, @@ -80,7 +80,7 @@ export const URL_STATE_KEYS: Record<UrlStateType, KeyUrlState[]> = { export type LocationTypes = | CONSTANTS.caseDetails | CONSTANTS.casePage - | CONSTANTS.detectionsPage + | CONSTANTS.alertsPage | CONSTANTS.hostsDetails | CONSTANTS.hostsPage | CONSTANTS.networkDetails diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx index ef60967e70ac3b..221df436402dde 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx @@ -27,7 +27,7 @@ import { ALL_URL_STATE_KEYS, UrlStateToRedux, } from './types'; -import { SiemPageName } from '../../../app/types'; +import { SecurityPageName } from '../../../app/types'; function usePrevious(value: PreviousLocationUrlState) { const ref = useRef<PreviousLocationUrlState>(value); @@ -203,7 +203,7 @@ export const useUrlStateHooks = ({ } }); } else if (pathName !== prevProps.pathName) { - handleInitialize(type, pageName === SiemPageName.detections); + handleInitialize(type, pageName === SecurityPageName.alerts); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isInitializing, history, pathName, pageName, prevProps, urlState]); diff --git a/x-pack/plugins/security_solution/public/common/lib/compose/kibana_compose.tsx b/x-pack/plugins/security_solution/public/common/lib/compose/kibana_compose.tsx index f7c7c65318482d..47834f148c9102 100644 --- a/x-pack/plugins/security_solution/public/common/lib/compose/kibana_compose.tsx +++ b/x-pack/plugins/security_solution/public/common/lib/compose/kibana_compose.tsx @@ -14,7 +14,7 @@ import introspectionQueryResultData from '../../../graphql/introspection.json'; import { AppFrontendLibs } from '../lib'; import { getLinks } from './helpers'; -export function compose(core: CoreStart): AppFrontendLibs { +export function composeLibs(core: CoreStart): AppFrontendLibs { const cache = new InMemoryCache({ dataIdFromObject: () => null, fragmentMatcher: new IntrospectionFragmentMatcher({ diff --git a/x-pack/plugins/security_solution/public/common/utils/route/index.test.tsx b/x-pack/plugins/security_solution/public/common/utils/route/index.test.tsx index 95e40b0f66301a..7246259f5afa1b 100644 --- a/x-pack/plugins/security_solution/public/common/utils/route/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/utils/route/index.test.tsx @@ -71,13 +71,13 @@ describe('Spy Routes', () => { path: pathname, url: pathname, params: { - pageName: undefined, detailName: '', tabName: HostsTableType.hosts, search: '', flowTarget: undefined, }, }} + pageName={undefined} /> </ManageRoutesSpy> ); @@ -103,13 +103,13 @@ describe('Spy Routes', () => { path: pathname, url: pathname, params: { - pageName: 'hosts', detailName: undefined, tabName: HostsTableType.hosts, search: '?IdoNotWantToSeeYou="true"', flowTarget: undefined, }, }} + pageName="hosts" /> </ManageRoutesSpy> ); @@ -124,9 +124,9 @@ describe('Spy Routes', () => { expect(dispatchMock.mock.calls[1]).toEqual([ { route: { + pageName: 'hosts', detailName: undefined, history: mockHistory, - pageName: 'hosts', pathName: pathname, tabName: HostsTableType.hosts, }, @@ -154,13 +154,13 @@ describe('Spy Routes', () => { path: pathname, url: pathname, params: { - pageName: 'hosts', detailName: undefined, tabName: HostsTableType.hosts, search: '?IdoNotWantToSeeYou="true"', flowTarget: undefined, }, }} + pageName="hosts" /> ); diff --git a/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx b/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx index 072bbcf31c9009..589436b945a657 100644 --- a/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx +++ b/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx @@ -12,13 +12,16 @@ import deepEqual from 'fast-deep-equal'; import { SpyRouteProps } from './types'; import { useRouteSpy } from './use_route_spy'; -export const SpyRouteComponent = memo<SpyRouteProps & { location: H.Location }>( +export const SpyRouteComponent = memo< + SpyRouteProps & { location: H.Location; pageName: string | undefined } +>( ({ location: { pathname, search }, history, match: { - params: { pageName, detailName, tabName, flowTarget }, + params: { detailName, tabName, flowTarget }, }, + pageName, state, }) => { const [isInitializing, setIsInitializing] = useState(true); diff --git a/x-pack/plugins/security_solution/public/common/utils/route/types.ts b/x-pack/plugins/security_solution/public/common/utils/route/types.ts index 912da545a66a3c..8656f20c929591 100644 --- a/x-pack/plugins/security_solution/public/common/utils/route/types.ts +++ b/x-pack/plugins/security_solution/public/common/utils/route/types.ts @@ -60,7 +60,6 @@ export interface ManageRoutesSpyProps { } export type SpyRouteProps = RouteComponentProps<{ - pageName: string | undefined; detailName: string | undefined; tabName: HostsTableType | undefined; search: string; diff --git a/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.tsx b/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.tsx index 37c6a6583f0fb9..fde3f6f8b222d8 100644 --- a/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.tsx +++ b/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.tsx @@ -4,21 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useLocation } from 'react-router-dom'; - import { useState, useEffect } from 'react'; -import { SiemPageName } from '../../../app/types'; +import { useRouteSpy } from '../route/use_route_spy'; -const hideTimelineForRoutes = [`/${SiemPageName.case}/configure`]; +const hideTimelineForRoutes = [`/cases/configure`]; export const useShowTimeline = () => { - const currentLocation = useLocation(); + const [{ pageName, pathName }] = useRouteSpy(); + const [showTimeline, setShowTimeline] = useState( - !hideTimelineForRoutes.includes(currentLocation.pathname) + !hideTimelineForRoutes.includes(window.location.pathname) ); useEffect(() => { - if (hideTimelineForRoutes.includes(currentLocation.pathname)) { + if ( + hideTimelineForRoutes.filter((route) => window.location.pathname.includes(route)).length > 0 + ) { if (showTimeline) { setShowTimeline(false); } @@ -26,7 +27,7 @@ export const useShowTimeline = () => { setShowTimeline(true); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentLocation.pathname]); + }, [pageName, pathName]); return [showTimeline]; }; diff --git a/x-pack/plugins/security_solution/public/endpoint_alerts/index.ts b/x-pack/plugins/security_solution/public/endpoint_alerts/index.ts index e7e45b95ceb91c..a755b53728e1a9 100644 --- a/x-pack/plugins/security_solution/public/endpoint_alerts/index.ts +++ b/x-pack/plugins/security_solution/public/endpoint_alerts/index.ts @@ -6,7 +6,7 @@ import { Reducer } from 'redux'; import { SecuritySubPluginWithStore } from '../app/types'; -import { endpointAlertsRoutes } from './routes'; +import { EndpointAlertsRoutes } from './routes'; import { alertListReducer } from './store/reducer'; import { AlertListState } from '../../common/endpoint_alerts/types'; import { alertMiddlewareFactory } from './store/middleware'; @@ -47,7 +47,7 @@ export class EndpointAlerts { ]; return { - routes: endpointAlertsRoutes(), + SubPluginRoutes: EndpointAlertsRoutes, store: { initialState: { alertList: undefined }, /** diff --git a/x-pack/plugins/security_solution/public/endpoint_alerts/routes.tsx b/x-pack/plugins/security_solution/public/endpoint_alerts/routes.tsx index d62ef20c384dcc..acc6a82e29a2cf 100644 --- a/x-pack/plugins/security_solution/public/endpoint_alerts/routes.tsx +++ b/x-pack/plugins/security_solution/public/endpoint_alerts/routes.tsx @@ -5,12 +5,14 @@ */ import React from 'react'; -import { Route } from 'react-router-dom'; +import { Route, Switch } from 'react-router-dom'; import { AlertIndex } from './view'; -export const endpointAlertsRoutes = () => [ - <Route path="/:pageName(endpoint-alerts)"> - <AlertIndex /> - </Route>, -]; +export const EndpointAlertsRoutes: React.FC = () => ( + <Switch> + <Route path="/:pageName(endpoint-alerts)"> + <AlertIndex /> + </Route> + </Switch> +); diff --git a/x-pack/plugins/security_solution/public/helpers.ts b/x-pack/plugins/security_solution/public/helpers.ts new file mode 100644 index 00000000000000..0dd66d06b78be2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/helpers.ts @@ -0,0 +1,75 @@ +/* + * 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 { CoreStart } from '../../../../src/core/public'; +import { APP_ID } from '../common/constants'; +import { SecurityPageName } from './app/types'; + +export const manageOldSiemRoutes = async (coreStart: CoreStart) => { + const { application } = coreStart; + const hashPath = window.location.hash.split('?'); + const search = hashPath.length >= 1 ? hashPath[1] : ''; + const pageRoute = hashPath.length > 0 ? hashPath[0].split('/') : []; + const pageName = pageRoute.length >= 1 ? pageRoute[1] : ''; + const path = `/${pageRoute.slice(2).join('/') ?? ''}?${search}`; + + switch (pageName) { + case SecurityPageName.overview: + application.navigateToApp(`${APP_ID}:${SecurityPageName.overview}`, { + replace: true, + path, + }); + break; + case 'ml-hosts': + application.navigateToApp(`${APP_ID}:${SecurityPageName.hosts}`, { + replace: true, + path: `/ml-hosts${path}`, + }); + break; + case SecurityPageName.hosts: + application.navigateToApp(`${APP_ID}:${SecurityPageName.hosts}`, { + replace: true, + path, + }); + break; + case 'ml-network': + application.navigateToApp(`${APP_ID}:${SecurityPageName.network}`, { + replace: true, + path: `/ml-network${path}`, + }); + break; + case SecurityPageName.network: + application.navigateToApp(`${APP_ID}:${SecurityPageName.network}`, { + replace: true, + path, + }); + break; + case SecurityPageName.timelines: + application.navigateToApp(`${APP_ID}:${SecurityPageName.timelines}`, { + replace: true, + path, + }); + break; + case SecurityPageName.case: + application.navigateToApp(`${APP_ID}:${SecurityPageName.case}`, { + replace: true, + path, + }); + break; + case 'detections': + application.navigateToApp(`${APP_ID}:${SecurityPageName.alerts}`, { + replace: true, + path, + }); + break; + default: + application.navigateToApp(`${APP_ID}:${SecurityPageName.overview}`, { + replace: true, + path: `?${search}`, + }); + break; + } +}; diff --git a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx index 1168f4f7454c81..1231c35f214603 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx @@ -33,6 +33,8 @@ jest.mock('../../../common/components/query_bar', () => ({ QueryBar: () => null, })); +jest.mock('../../../common/components/link_to'); + describe('Hosts Table', () => { const loadPage = jest.fn(); const state: State = mockGlobalState; diff --git a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx index 7528c148f24569..a21d932e837df5 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx @@ -18,6 +18,8 @@ import { mockData } from './mock'; import { HostsType } from '../../store/model'; import * as i18n from './translations'; +jest.mock('../../../common/components/link_to'); + describe('Uncommon Process Table Component', () => { const loadPage = jest.fn(); const mount = useMountAppended(); diff --git a/x-pack/plugins/security_solution/public/hosts/index.ts b/x-pack/plugins/security_solution/public/hosts/index.ts index 90d5f54a027d7f..d7ee1a53b798df 100644 --- a/x-pack/plugins/security_solution/public/hosts/index.ts +++ b/x-pack/plugins/security_solution/public/hosts/index.ts @@ -8,7 +8,7 @@ import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { TimelineIdLiteral, TimelineId } from '../../common/types/timeline'; import { SecuritySubPluginWithStore } from '../app/types'; import { getTimelinesInStorageByIds } from '../timelines/containers/local_storage'; -import { getHostsRoutes } from './routes'; +import { HostsRoutes } from './routes'; import { initialHostsState, hostsReducer, HostsState } from './store'; const HOST_TIMELINE_IDS: TimelineIdLiteral[] = [ @@ -21,7 +21,7 @@ export class Hosts { public start(storage: Storage): SecuritySubPluginWithStore<'hosts', HostsState> { return { - routes: getHostsRoutes(), + SubPluginRoutes: HostsRoutes, storageTimelines: { timelineById: getTimelinesInStorageByIds(storage, HOST_TIMELINE_IDS), }, diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx index fa76dc93375e01..936789625a4dde 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx @@ -71,7 +71,7 @@ describe('body', () => { test(`it should pass expected object properties to ${componentName}`, () => { const wrapper = mount( <TestProviders> - <MemoryRouter initialEntries={[`/hosts/host-1/${path}`]}> + <MemoryRouter initialEntries={[`/host-1/${path}`]}> <HostDetailsTabs from={0} isInitializing={false} diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx index a5fabf4d515f86..e3f00a377d2724 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx @@ -9,6 +9,7 @@ import React, { useEffect, useCallback, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { StickyContainer } from 'react-sticky'; +import { SecurityPageName } from '../../../app/types'; import { UpdateDateRange } from '../../../common/components/charts/common'; import { FiltersGlobal } from '../../../common/components/filters_global'; import { HeaderPage } from '../../../common/components/header_page'; @@ -209,7 +210,7 @@ const HostDetailsComponent = React.memo<HostDetailsProps & PropsFromRedux>( }} </WithSource> - <SpyRoute /> + <SpyRoute pageName={SecurityPageName.hosts} /> </> ); } diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/nav_tabs.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/nav_tabs.tsx index 4d04d16580a638..0d30ef32e5d984 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/nav_tabs.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/nav_tabs.tsx @@ -8,10 +8,10 @@ import { omit } from 'lodash/fp'; import * as i18n from '../translations'; import { HostDetailsNavTab } from './types'; import { HostsTableType } from '../../store/model'; -import { SiemPageName } from '../../../app/types'; +import { SecurityPageName } from '../../../app/types'; const getTabsOnHostDetailsUrl = (hostName: string, tabName: HostsTableType) => - `#/${SiemPageName.hosts}/${hostName}/${tabName}`; + `/${hostName}/${tabName}`; export const navTabsHostDetails = ( hostName: string, @@ -25,6 +25,7 @@ export const navTabsHostDetails = ( disabled: false, urlKey: 'host', isDetailPage: true, + pageId: SecurityPageName.hosts, }, [HostsTableType.uncommonProcesses]: { id: HostsTableType.uncommonProcesses, @@ -33,6 +34,7 @@ export const navTabsHostDetails = ( disabled: false, urlKey: 'host', isDetailPage: true, + pageId: SecurityPageName.hosts, }, [HostsTableType.anomalies]: { id: HostsTableType.anomalies, @@ -41,6 +43,7 @@ export const navTabsHostDetails = ( disabled: false, urlKey: 'host', isDetailPage: true, + pageId: SecurityPageName.hosts, }, [HostsTableType.events]: { id: HostsTableType.events, @@ -49,6 +52,7 @@ export const navTabsHostDetails = ( disabled: false, urlKey: 'host', isDetailPage: true, + pageId: SecurityPageName.hosts, }, [HostsTableType.alerts]: { id: HostsTableType.alerts, @@ -56,6 +60,7 @@ export const navTabsHostDetails = ( href: getTabsOnHostDetailsUrl(hostName, HostsTableType.alerts), disabled: false, urlKey: 'host', + pageId: SecurityPageName.hosts, }, }; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/types.ts b/x-pack/plugins/security_solution/public/hosts/pages/details/types.ts index f145abed2d8ffc..aa6288d473c910 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/types.ts +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/types.ts @@ -7,7 +7,6 @@ import { ActionCreator } from 'typescript-fsa'; import { Query, IIndexPattern, Filter } from 'src/plugins/data/public'; import { InputsModelId } from '../../../common/store/inputs/constants'; -import { HostComponentProps } from '../../../common/components/link_to/redirect_to_hosts'; import { HostsTableType } from '../../store/model'; import { HostsQueryProps } from '../types'; import { NavTab } from '../../../common/components/navigation/types'; @@ -40,7 +39,6 @@ export interface HostDetailsProps extends HostsQueryProps { export type HostDetailsComponentProps = HostDetailsComponentReduxProps & HostDetailsComponentDispatchProps & - HostComponentProps & HostsQueryProps; type KeyHostDetailsNavTabWithoutMlPermission = HostsTableType.authentications & diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/utils.ts b/x-pack/plugins/security_solution/public/hosts/pages/details/utils.ts index d45cb3368b4e19..5c5c7283eee474 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/utils.ts +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/utils.ts @@ -10,13 +10,13 @@ import { get, isEmpty } from 'lodash/fp'; import { ChromeBreadcrumb } from '../../../../../../../src/core/public'; import { hostsModel } from '../../store'; import { HostsTableType } from '../../store/model'; -import { - getHostsUrl, - getHostDetailsUrl, -} from '../../../common/components/link_to/redirect_to_hosts'; +import { getHostDetailsUrl } from '../../../common/components/link_to/redirect_to_hosts'; import * as i18n from '../translations'; import { HostRouteSpyState } from '../../../common/utils/route/types'; +import { GetUrlForApp } from '../../../common/components/navigation/types'; +import { APP_ID } from '../../../../common/constants'; +import { SecurityPageName } from '../../../app/types'; export const type = hostsModel.HostsType.details; @@ -29,11 +29,17 @@ const TabNameMappedToI18nKey: Record<HostsTableType, string> = { [HostsTableType.alerts]: i18n.NAVIGATION_ALERTS_TITLE, }; -export const getBreadcrumbs = (params: HostRouteSpyState, search: string[]): ChromeBreadcrumb[] => { +export const getBreadcrumbs = ( + params: HostRouteSpyState, + search: string[], + getUrlForApp: GetUrlForApp +): ChromeBreadcrumb[] => { let breadcrumb = [ { text: i18n.PAGE_TITLE, - href: `${getHostsUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, + href: getUrlForApp(`${APP_ID}:${SecurityPageName.hosts}`, { + path: !isEmpty(search[0]) ? search[0] : '', + }), }, ]; @@ -42,7 +48,9 @@ export const getBreadcrumbs = (params: HostRouteSpyState, search: string[]): Chr ...breadcrumb, { text: params.detailName, - href: `${getHostDetailsUrl(params.detailName)}${!isEmpty(search[1]) ? search[1] : ''}`, + href: getUrlForApp(`${APP_ID}:${SecurityPageName.hosts}`, { + path: getHostDetailsUrl(params.detailName, !isEmpty(search[0]) ? search[0] : ''), + }), }, ]; } diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx index f7583f65a4fcd4..f6429544f855e0 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx @@ -8,8 +8,9 @@ import { EuiSpacer } from '@elastic/eui'; import React, { useCallback } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { StickyContainer } from 'react-sticky'; - import { useParams } from 'react-router-dom'; + +import { SecurityPageName } from '../../app/types'; import { UpdateDateRange } from '../../common/components/charts/common'; import { FiltersGlobal } from '../../common/components/filters_global'; import { HeaderPage } from '../../common/components/header_page'; @@ -158,7 +159,7 @@ export const HostsComponent = React.memo<HostsComponentProps & PropsFromRedux>( }} </WithSource> - <SpyRoute /> + <SpyRoute pageName={SecurityPageName.hosts} /> </> ); } diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts_tabs.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts_tabs.tsx index 549c198a435263..e2a0ba0f1fe6ca 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts_tabs.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts_tabs.tsx @@ -70,22 +70,22 @@ export const HostsTabs = memo<HostsTabsProps>( return ( <Switch> - <Route path={`${hostsPagePath}/:tabName(${HostsTableType.hosts})`}> + <Route path={`/:tabName(${HostsTableType.hosts})`}> <HostsQueryTabBody {...tabProps} /> </Route> - <Route path={`${hostsPagePath}/:tabName(${HostsTableType.authentications})`}> + <Route path={`/:tabName(${HostsTableType.authentications})`}> <AuthenticationsQueryTabBody {...tabProps} /> </Route> - <Route path={`${hostsPagePath}/:tabName(${HostsTableType.uncommonProcesses})`}> + <Route path={`/:tabName(${HostsTableType.uncommonProcesses})`}> <UncommonProcessQueryTabBody {...tabProps} /> </Route> - <Route path={`${hostsPagePath}/:tabName(${HostsTableType.anomalies})`}> + <Route path={`/:tabName(${HostsTableType.anomalies})`}> <AnomaliesQueryTabBody {...tabProps} AnomaliesTableComponent={AnomaliesHostTable} /> </Route> - <Route path={`${hostsPagePath}/:tabName(${HostsTableType.events})`}> + <Route path={`/:tabName(${HostsTableType.events})`}> <EventsQueryTabBody {...tabProps} /> </Route> - <Route path={`${hostsPagePath}/:tabName(${HostsTableType.alerts})`}> + <Route path={`/:tabName(${HostsTableType.alerts})`}> <HostAlertsQueryTabBody {...tabProps} /> </Route> </Switch> diff --git a/x-pack/plugins/security_solution/public/hosts/pages/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/index.tsx index 336abc60e5ba16..c2285cf0a97e1f 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/index.tsx @@ -5,18 +5,18 @@ */ import React from 'react'; -import { Redirect, Route, Switch, RouteComponentProps } from 'react-router-dom'; +import { Route, Switch, RouteComponentProps, useHistory } from 'react-router-dom'; import { HostDetails } from './details'; import { HostsTableType } from '../store/model'; +import { MlHostConditionalContainer } from '../../common/components/ml/conditional_links/ml_host_conditional_container'; import { GlobalTime } from '../../common/containers/global_time'; -import { SiemPageName } from '../../app/types'; import { Hosts } from './hosts'; import { hostsPagePath, hostDetailsPagePath } from './types'; -const getHostsTabPath = (pagePath: string) => - `${pagePath}/:tabName(` + +const getHostsTabPath = () => + `/:tabName(` + `${HostsTableType.hosts}|` + `${HostsTableType.authentications}|` + `${HostsTableType.uncommonProcesses}|` + @@ -34,62 +34,75 @@ const getHostDetailsTabPath = (pagePath: string) => type Props = Partial<RouteComponentProps<{}>> & { url: string }; -export const HostsContainer = React.memo<Props>(({ url }) => ( - <GlobalTime> - {({ to, from, setQuery, deleteQuery, isInitializing }) => ( - <Switch> - <Route - strict - exact - path={getHostsTabPath(hostsPagePath)} - render={() => ( - <Hosts - hostsPagePath={hostsPagePath} - from={from} - to={to} - setQuery={setQuery} - isInitializing={isInitializing} - deleteQuery={deleteQuery} - /> - )} - /> - <Route - strict - path={getHostDetailsTabPath(hostsPagePath)} - render={({ - match: { - params: { detailName }, - }, - }) => ( - <HostDetails - hostDetailsPagePath={hostDetailsPagePath} - detailName={detailName} - from={from} - to={to} - setQuery={setQuery} - isInitializing={isInitializing} - deleteQuery={deleteQuery} - /> - )} - /> - <Route - path={hostDetailsPagePath} - render={({ - match: { - params: { detailName }, - }, - location: { search = '' }, - }) => <Redirect to={`${url}/${detailName}/${HostsTableType.authentications}${search}`} />} - /> - <Route - path={`${hostsPagePath}/`} - render={({ location: { search = '' } }) => ( - <Redirect to={`/${SiemPageName.hosts}/${HostsTableType.hosts}${search}`} /> - )} - /> - </Switch> - )} - </GlobalTime> -)); +export const HostsContainer = React.memo<Props>(({ url }) => { + const history = useHistory(); + return ( + <GlobalTime> + {({ to, from, setQuery, deleteQuery, isInitializing }) => ( + <Switch> + <Route + path="/ml-hosts" + render={({ location, match }) => ( + <MlHostConditionalContainer location={location} url={match.url} /> + )} + /> + <Route + path={getHostsTabPath()} + render={() => ( + <Hosts + hostsPagePath={hostsPagePath} + from={from} + to={to} + setQuery={setQuery} + isInitializing={isInitializing} + deleteQuery={deleteQuery} + /> + )} + /> + <Route + path={getHostDetailsTabPath(hostsPagePath)} + render={({ + match: { + params: { detailName }, + }, + }) => ( + <HostDetails + hostDetailsPagePath={hostDetailsPagePath} + detailName={detailName} + from={from} + to={to} + setQuery={setQuery} + isInitializing={isInitializing} + deleteQuery={deleteQuery} + /> + )} + /> + <Route + path={hostDetailsPagePath} + render={({ + match: { + params: { detailName }, + }, + location: { search = '' }, + }) => { + history.replace(`${detailName}/${HostsTableType.authentications}${search}`); + return null; + }} + /> + + <Route + exact + strict + path="" + render={({ location: { search = '' } }) => { + history.replace(`${HostsTableType.hosts}${search}`); + return null; + }} + /> + </Switch> + )} + </GlobalTime> + ); +}); HostsContainer.displayName = 'HostsContainer'; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/nav_tabs.tsx b/x-pack/plugins/security_solution/public/hosts/pages/nav_tabs.tsx index 9bab3f7efe74af..dd1d3b1508925c 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/nav_tabs.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/nav_tabs.tsx @@ -8,9 +8,9 @@ import { omit } from 'lodash/fp'; import * as i18n from './translations'; import { HostsTableType } from '../store/model'; import { HostsNavTab } from './navigation/types'; -import { SiemPageName } from '../../app/types'; +import { SecurityPageName } from '../../app/types'; -const getTabsOnHostsUrl = (tabName: HostsTableType) => `#/${SiemPageName.hosts}/${tabName}`; +const getTabsOnHostsUrl = (tabName: HostsTableType) => `/${tabName}`; export const navTabsHosts = (hasMlUserPermissions: boolean): HostsNavTab => { const hostsNavTabs = { @@ -20,6 +20,7 @@ export const navTabsHosts = (hasMlUserPermissions: boolean): HostsNavTab => { href: getTabsOnHostsUrl(HostsTableType.hosts), disabled: false, urlKey: 'host', + pageId: SecurityPageName.hosts, }, [HostsTableType.authentications]: { id: HostsTableType.authentications, @@ -27,6 +28,7 @@ export const navTabsHosts = (hasMlUserPermissions: boolean): HostsNavTab => { href: getTabsOnHostsUrl(HostsTableType.authentications), disabled: false, urlKey: 'host', + pageId: SecurityPageName.hosts, }, [HostsTableType.uncommonProcesses]: { id: HostsTableType.uncommonProcesses, @@ -34,6 +36,7 @@ export const navTabsHosts = (hasMlUserPermissions: boolean): HostsNavTab => { href: getTabsOnHostsUrl(HostsTableType.uncommonProcesses), disabled: false, urlKey: 'host', + pageId: SecurityPageName.hosts, }, [HostsTableType.anomalies]: { id: HostsTableType.anomalies, @@ -41,6 +44,7 @@ export const navTabsHosts = (hasMlUserPermissions: boolean): HostsNavTab => { href: getTabsOnHostsUrl(HostsTableType.anomalies), disabled: false, urlKey: 'host', + pageId: SecurityPageName.hosts, }, [HostsTableType.events]: { id: HostsTableType.events, @@ -48,6 +52,7 @@ export const navTabsHosts = (hasMlUserPermissions: boolean): HostsNavTab => { href: getTabsOnHostsUrl(HostsTableType.events), disabled: false, urlKey: 'host', + pageId: SecurityPageName.hosts, }, [HostsTableType.alerts]: { id: HostsTableType.alerts, @@ -55,6 +60,7 @@ export const navTabsHosts = (hasMlUserPermissions: boolean): HostsNavTab => { href: getTabsOnHostsUrl(HostsTableType.alerts), disabled: false, urlKey: 'host', + pageId: SecurityPageName.hosts, }, }; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/types.ts b/x-pack/plugins/security_solution/public/hosts/pages/types.ts index 229349f390ecdb..ffd17b0ef46f6e 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/types.ts +++ b/x-pack/plugins/security_solution/public/hosts/pages/types.ts @@ -7,13 +7,12 @@ import { IIndexPattern } from 'src/plugins/data/public'; import { ActionCreator } from 'typescript-fsa'; -import { SiemPageName } from '../../app/types'; import { hostsModel } from '../store'; import { GlobalTimeArgs } from '../../common/containers/global_time'; import { InputsModelId } from '../../common/store/inputs/constants'; -export const hostsPagePath = `/:pageName(${SiemPageName.hosts})`; -export const hostDetailsPagePath = `${hostsPagePath}/:detailName`; +export const hostsPagePath = '/'; +export const hostDetailsPagePath = `/:detailName`; export type HostsTabsProps = HostsComponentProps & { filterQuery: string; diff --git a/x-pack/plugins/security_solution/public/hosts/routes.tsx b/x-pack/plugins/security_solution/public/hosts/routes.tsx index 93585fa0f83943..e6b815cfb50366 100644 --- a/x-pack/plugins/security_solution/public/hosts/routes.tsx +++ b/x-pack/plugins/security_solution/public/hosts/routes.tsx @@ -5,14 +5,14 @@ */ import React from 'react'; -import { Route } from 'react-router-dom'; +import { Route, Switch } from 'react-router-dom'; import { HostsContainer } from './pages'; -import { SiemPageName } from '../app/types'; +import { NotFoundPage } from '../app/404'; -export const getHostsRoutes = () => [ - <Route - path={`/:pageName(${SiemPageName.hosts})`} - render={({ match }) => <HostsContainer url={match.url} />} - />, -]; +export const HostsRoutes = () => ( + <Switch> + <Route path="/" render={({ match }) => <HostsContainer url={match.url} />} /> + <Route render={() => <NotFoundPage />} /> + </Switch> +); diff --git a/x-pack/plugins/security_solution/public/management/common/constants.ts b/x-pack/plugins/security_solution/public/management/common/constants.ts index 95293ccc9cf8cd..7456be1d6784d3 100644 --- a/x-pack/plugins/security_solution/public/management/common/constants.ts +++ b/x-pack/plugins/security_solution/public/management/common/constants.ts @@ -3,11 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { SiemPageName } from '../../app/types'; import { ManagementStoreGlobalNamespace, ManagementSubTab } from '../types'; // --[ ROUTING ]--------------------------------------------------------------------------- -export const MANAGEMENT_ROUTING_ROOT_PATH = `/:pageName(${SiemPageName.management})`; +export const MANAGEMENT_ROUTING_ROOT_PATH = ''; export const MANAGEMENT_ROUTING_ENDPOINTS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.endpoints})`; export const MANAGEMENT_ROUTING_POLICIES_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.policies})`; export const MANAGEMENT_ROUTING_POLICY_DETAILS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.policies})/:policyId`; diff --git a/x-pack/plugins/security_solution/public/management/common/routing.ts b/x-pack/plugins/security_solution/public/management/common/routing.ts index bc204535e5abce..92eb7717318d32 100644 --- a/x-pack/plugins/security_solution/public/management/common/routing.ts +++ b/x-pack/plugins/security_solution/public/management/common/routing.ts @@ -4,17 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ +import { isEmpty } from 'lodash/fp'; import { generatePath } from 'react-router-dom'; // eslint-disable-next-line import/no-nodejs-modules import querystring from 'querystring'; + import { MANAGEMENT_ROUTING_ENDPOINTS_PATH, MANAGEMENT_ROUTING_POLICIES_PATH, MANAGEMENT_ROUTING_POLICY_DETAILS_PATH, - MANAGEMENT_ROUTING_ROOT_PATH, } from './constants'; import { ManagementSubTab } from '../types'; -import { SiemPageName } from '../../app/types'; +import { appendSearch } from '../../common/components/link_to/helpers'; import { HostIndexUIQueryParams } from '../pages/endpoint_hosts/types'; // Taken from: https://github.com/microsoft/TypeScript/issues/12936#issuecomment-559034150 @@ -34,76 +35,49 @@ const querystringStringify: <ExpectedType extends object, ArgType>( type EndpointDetailsUrlProps = Omit<HostIndexUIQueryParams, 'selected_host'> & Required<Pick<HostIndexUIQueryParams, 'selected_host'>>; -/** - * Input props for the `getManagementUrl()` method - */ -export type GetManagementUrlProps = { - /** - * Exclude the URL prefix (everything to the left of where the router was mounted. - * This may be needed when interacting with react-router (ex. to do `history.push()` or - * validations against matched path) - */ - excludePrefix?: boolean; -} & ( - | ({ name: 'default' | 'endpointList' } & HostIndexUIQueryParams) - | ({ name: 'endpointDetails' | 'endpointPolicyResponse' } & EndpointDetailsUrlProps) - | { name: 'policyList' } - | { name: 'policyDetails'; policyId: string } -); - -// Prefix is (almost) everything to the left of where the Router was mounted. In SIEM, since -// we're using Hash router, thats the `#`. -const URL_PREFIX = '#'; - -/** - * Returns a URL string for a given Management page view - * @param props - */ -export const getManagementUrl = (props: GetManagementUrlProps): string => { - let url = props.excludePrefix ? '' : URL_PREFIX; - - if (props.name === 'default' || props.name === 'endpointList') { - const { name, excludePrefix, ...queryParams } = props; - const urlQueryParams = querystringStringify<HostIndexUIQueryParams, typeof queryParams>( - queryParams - ); - - if (name === 'endpointList') { - url += generatePath(MANAGEMENT_ROUTING_ENDPOINTS_PATH, { - pageName: SiemPageName.management, - tabName: ManagementSubTab.endpoints, - }); - } else { - url += generatePath(MANAGEMENT_ROUTING_ROOT_PATH, { - pageName: SiemPageName.management, - }); - } +export const getEndpointListPath = ( + props: { name: 'default' | 'endpointList' } & HostIndexUIQueryParams, + search?: string +) => { + const { name, ...queryParams } = props; + const urlQueryParams = querystringStringify<HostIndexUIQueryParams, typeof queryParams>( + queryParams + ); + const urlSearch = `${urlQueryParams && !isEmpty(search) ? '&' : ''}${search ?? ''}`; - if (urlQueryParams) { - url += `?${urlQueryParams}`; - } - } else if (props.name === 'endpointDetails' || props.name === 'endpointPolicyResponse') { - const { name, excludePrefix, ...queryParams } = props; - queryParams.show = (props.name === 'endpointPolicyResponse' - ? 'policy_response' - : '') as HostIndexUIQueryParams['show']; - - url += `${generatePath(MANAGEMENT_ROUTING_ENDPOINTS_PATH, { - pageName: SiemPageName.management, + if (name === 'endpointList') { + return `${generatePath(MANAGEMENT_ROUTING_ENDPOINTS_PATH, { tabName: ManagementSubTab.endpoints, - })}?${querystringStringify<EndpointDetailsUrlProps, typeof queryParams>(queryParams)}`; - } else if (props.name === 'policyList') { - url += generatePath(MANAGEMENT_ROUTING_POLICIES_PATH, { - pageName: SiemPageName.management, - tabName: ManagementSubTab.policies, - }); - } else if (props.name === 'policyDetails') { - url += generatePath(MANAGEMENT_ROUTING_POLICY_DETAILS_PATH, { - pageName: SiemPageName.management, - tabName: ManagementSubTab.policies, - policyId: props.policyId, - }); + })}${appendSearch(`${urlQueryParams ? `${urlQueryParams}${urlSearch}` : urlSearch}`)}`; } + return `${appendSearch(`${urlQueryParams ? `${urlQueryParams}${urlSearch}` : urlSearch}`)}`; +}; - return url; +export const getEndpointDetailsPath = ( + props: { name: 'endpointDetails' | 'endpointPolicyResponse' } & EndpointDetailsUrlProps, + search?: string +) => { + const { name, ...queryParams } = props; + queryParams.show = (props.name === 'endpointPolicyResponse' + ? 'policy_response' + : '') as HostIndexUIQueryParams['show']; + const urlQueryParams = querystringStringify<EndpointDetailsUrlProps, typeof queryParams>( + queryParams + ); + const urlSearch = `${urlQueryParams && !isEmpty(search) ? '&' : ''}${search ?? ''}`; + + return `${generatePath(MANAGEMENT_ROUTING_ENDPOINTS_PATH, { + tabName: ManagementSubTab.endpoints, + })}${appendSearch(`${urlQueryParams ? `${urlQueryParams}${urlSearch}` : urlSearch}`)}`; }; + +export const getPoliciesPath = (search?: string) => + `${generatePath(MANAGEMENT_ROUTING_POLICIES_PATH, { + tabName: ManagementSubTab.policies, + })}${appendSearch(search)}`; + +export const getPolicyDetailPath = (policyId: string, search?: string) => + `${generatePath(MANAGEMENT_ROUTING_POLICY_DETAILS_PATH, { + tabName: ManagementSubTab.policies, + policyId, + })}${appendSearch(search)}`; diff --git a/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx b/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx index 8f7e0e06fb7a16..c3dbb93b369a97 100644 --- a/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx +++ b/x-pack/plugins/security_solution/public/management/components/management_page_view.tsx @@ -9,10 +9,21 @@ import { i18n } from '@kbn/i18n'; import { useParams } from 'react-router-dom'; import { PageView, PageViewProps } from '../../common/components/endpoint/page_view'; import { ManagementSubTab } from '../types'; -import { getManagementUrl } from '..'; +import { SecurityPageName } from '../../app/types'; +import { useFormatUrl } from '../../common/components/link_to'; +import { getEndpointListPath, getPoliciesPath } from '../common/routing'; +import { useNavigateByRouterEventHandler } from '../../common/hooks/endpoint/use_navigate_by_router_event_handler'; export const ManagementPageView = memo<Omit<PageViewProps, 'tabs'>>((options) => { + const { formatUrl, search } = useFormatUrl(SecurityPageName.management); const { tabName } = useParams<{ tabName: ManagementSubTab }>(); + + const goToEndpoint = useNavigateByRouterEventHandler( + getEndpointListPath({ name: 'endpointList' }, search) + ); + + const goToPolicies = useNavigateByRouterEventHandler(getPoliciesPath(search)); + const tabs = useMemo((): PageViewProps['tabs'] | undefined => { if (options.viewType === 'details') { return undefined; @@ -24,7 +35,8 @@ export const ManagementPageView = memo<Omit<PageViewProps, 'tabs'>>((options) => }), id: ManagementSubTab.endpoints, isSelected: tabName === ManagementSubTab.endpoints, - href: getManagementUrl({ name: 'endpointList' }), + href: formatUrl(getEndpointListPath({ name: 'endpointList' })), + onClick: goToEndpoint, }, { name: i18n.translate('xpack.securitySolution.managementTabs.policies', { @@ -32,10 +44,11 @@ export const ManagementPageView = memo<Omit<PageViewProps, 'tabs'>>((options) => }), id: ManagementSubTab.policies, isSelected: tabName === ManagementSubTab.policies, - href: getManagementUrl({ name: 'policyList' }), + href: formatUrl(getPoliciesPath()), + onClick: goToPolicies, }, ]; - }, [options.viewType, tabName]); + }, [formatUrl, goToEndpoint, goToPolicies, options.viewType, tabName]); return <PageView {...options} tabs={tabs} />; }); diff --git a/x-pack/plugins/security_solution/public/management/index.ts b/x-pack/plugins/security_solution/public/management/index.ts index d6a723e5340bfa..902ed085bd3698 100644 --- a/x-pack/plugins/security_solution/public/management/index.ts +++ b/x-pack/plugins/security_solution/public/management/index.ts @@ -6,7 +6,7 @@ import { CoreStart } from 'kibana/public'; import { Reducer, CombinedState } from 'redux'; -import { managementRoutes } from './routes'; +import { ManagementRoutes } from './routes'; import { StartPlugins } from '../types'; import { SecuritySubPluginWithStore } from '../app/types'; import { managementReducer } from './store/reducer'; @@ -14,8 +14,6 @@ import { AppAction } from '../common/store/actions'; import { managementMiddlewareFactory } from './store/middleware'; import { ManagementState } from './types'; -export { getManagementUrl } from './common/routing'; - /** * Internally, our state is sometimes immutable, ignore that in our external * interface. @@ -40,7 +38,7 @@ export class Management { plugins: StartPlugins ): SecuritySubPluginWithStore<'management', ManagementState> { return { - routes: managementRoutes(), + SubPluginRoutes: ManagementRoutes, store: { initialState: { management: undefined, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/routes.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/routes.tsx index 4ff9ecfaeab0e3..3f92358b93d56d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/routes.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/routes.tsx @@ -5,12 +5,14 @@ */ import React from 'react'; -import { Route } from 'react-router-dom'; +import { Route, Switch } from 'react-router-dom'; import { HostList } from './view'; -export const endpointHostsRoutes = () => [ - <Route path="/:pageName(endpoint-hosts)"> - <HostList /> - </Route>, -]; +export const EndpointHostsRoutes: React.FC = () => ( + <Switch> + <Route path="/:pageName(endpoint-hosts)"> + <HostList /> + </Route> + </Switch> +); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/host_pagination.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/host_pagination.test.ts index b8eaa39c777529..ae2ce9facc837f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/host_pagination.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/host_pagination.test.ts @@ -24,7 +24,7 @@ import { MiddlewareActionSpyHelper, createSpyMiddleware, } from '../../../../common/store/test_utils'; -import { getManagementUrl } from '../../..'; +import { getEndpointListPath } from '../../../common/routing'; describe('host list pagination: ', () => { let fakeCoreStart: jest.Mocked<CoreStart>; @@ -56,9 +56,7 @@ describe('host list pagination: ', () => { queryParams = () => uiQueryParams(store.getState()); historyPush = (nextQueryParams: HostIndexUIQueryParams): void => { - return history.push( - getManagementUrl({ name: 'endpointList', excludePrefix: true, ...nextQueryParams }) - ); + return history.push(getEndpointListPath({ name: 'endpointList', ...nextQueryParams })); }; }); @@ -72,7 +70,7 @@ describe('host list pagination: ', () => { type: 'userChangedUrl', payload: { ...history.location, - pathname: getManagementUrl({ name: 'endpointList', excludePrefix: true }), + pathname: getEndpointListPath({ name: 'endpointList' }), }, }); await waitForAction('serverReturnedHostList'); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts index a6cd2ca3afac45..e62c53e061a335 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts @@ -21,7 +21,7 @@ import { listData } from './selectors'; import { HostState } from '../types'; import { hostListReducer } from './reducer'; import { hostMiddlewareFactory } from './middleware'; -import { getManagementUrl } from '../../..'; +import { getEndpointListPath } from '../../../common/routing'; describe('host list middleware', () => { let fakeCoreStart: jest.Mocked<CoreStart>; @@ -60,7 +60,7 @@ describe('host list middleware', () => { type: 'userChangedUrl', payload: { ...history.location, - pathname: getManagementUrl({ name: 'endpointList', excludePrefix: true }), + pathname: getEndpointListPath({ name: 'endpointList' }), }, }); await waitForAction('serverReturnedHostList'); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx index f31b54b93851f0..b7e90c19799c76 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/host_details.tsx @@ -25,7 +25,9 @@ import { POLICY_STATUS_TO_HEALTH_COLOR } from '../host_constants'; import { FormattedDateAndTime } from '../../../../../common/components/endpoint/formatted_date_time'; import { useNavigateByRouterEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; import { LinkToApp } from '../../../../../common/components/endpoint/link_to_app'; -import { getManagementUrl } from '../../../..'; +import { getEndpointDetailsPath, getPolicyDetailPath } from '../../../../common/routing'; +import { SecurityPageName } from '../../../../../app/types'; +import { useFormatUrl } from '../../../../../common/components/link_to'; const HostIds = styled(EuiListGroupItem)` margin-top: 0; @@ -51,6 +53,8 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => { const policyStatus = useHostSelector( policyResponseStatus ) as keyof typeof POLICY_STATUS_TO_HEALTH_COLOR; + const { formatUrl } = useFormatUrl(SecurityPageName.management); + const detailsResultsUpper = useMemo(() => { return [ { @@ -77,35 +81,29 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => { const [policyResponseUri, policyResponseRoutePath] = useMemo(() => { const { selected_host, show, ...currentUrlParams } = queryParams; return [ - getManagementUrl({ + formatUrl( + getEndpointDetailsPath({ + name: 'endpointPolicyResponse', + ...currentUrlParams, + selected_host: details.host.id, + }) + ), + getEndpointDetailsPath({ name: 'endpointPolicyResponse', ...currentUrlParams, selected_host: details.host.id, }), - getManagementUrl({ - name: 'endpointPolicyResponse', - excludePrefix: true, - ...currentUrlParams, - selected_host: details.host.id, - }), ]; - }, [details.host.id, queryParams]); + }, [details.host.id, formatUrl, queryParams]); const policyStatusClickHandler = useNavigateByRouterEventHandler(policyResponseRoutePath); const [policyDetailsRoutePath, policyDetailsRouteUrl] = useMemo(() => { return [ - getManagementUrl({ - name: 'policyDetails', - policyId: details.Endpoint.policy.applied.id, - excludePrefix: true, - }), - getManagementUrl({ - name: 'policyDetails', - policyId: details.Endpoint.policy.applied.id, - }), + getPolicyDetailPath(details.Endpoint.policy.applied.id), + formatUrl(getPolicyDetailPath(details.Endpoint.policy.applied.id)), ]; - }, [details.Endpoint.policy.applied.id]); + }, [details.Endpoint.policy.applied.id, formatUrl]); const policyDetailsClickHandler = useNavigateByRouterEventHandler(policyDetailsRoutePath); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx index ed853e24d7c312..3d44b73858e902 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/index.tsx @@ -38,7 +38,9 @@ import { PolicyResponse } from './policy_response'; import { HostMetadata } from '../../../../../../common/endpoint/types'; import { FlyoutSubHeader, FlyoutSubHeaderProps } from './components/flyout_sub_header'; import { useNavigateByRouterEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; -import { getManagementUrl } from '../../../..'; +import { getEndpointListPath } from '../../../../common/routing'; +import { SecurityPageName } from '../../../../../app/types'; +import { useFormatUrl } from '../../../../../common/components/link_to'; export const HostDetailsFlyout = memo(() => { const history = useHistory(); @@ -116,21 +118,23 @@ const PolicyResponseFlyoutPanel = memo<{ const responseAttentionCount = useHostSelector(policyResponseFailedOrWarningActionCount); const loading = useHostSelector(policyResponseLoading); const error = useHostSelector(policyResponseError); + const { formatUrl } = useFormatUrl(SecurityPageName.management); const [detailsUri, detailsRoutePath] = useMemo( () => [ - getManagementUrl({ + formatUrl( + getEndpointListPath({ + name: 'endpointList', + ...queryParams, + selected_host: hostMeta.host.id, + }) + ), + getEndpointListPath({ name: 'endpointList', ...queryParams, selected_host: hostMeta.host.id, }), - getManagementUrl({ - name: 'endpointList', - excludePrefix: true, - ...queryParams, - selected_host: hostMeta.host.id, - }), ], - [hostMeta.host.id, queryParams] + [hostMeta.host.id, formatUrl, queryParams] ); const backToDetailsClickHandler = useNavigateByRouterEventHandler(detailsRoutePath); const backButtonProp = useMemo((): FlyoutSubHeaderProps['backButton'] => { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index a3e2a55fc75690..224411c5f7ec0d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -252,7 +252,7 @@ describe('when on the hosts page', () => { const policyDetailsLink = await renderResult.findByTestId('policyDetailsValue'); expect(policyDetailsLink).not.toBeNull(); expect(policyDetailsLink.getAttribute('href')).toEqual( - `#/management/policy/${hostDetails.metadata.Endpoint.policy.applied.id}` + `/policy/${hostDetails.metadata.Endpoint.policy.applied.id}` ); }); @@ -268,7 +268,7 @@ describe('when on the hosts page', () => { }); const changedUrlAction = await userChangedUrlChecker; expect(changedUrlAction.payload.pathname).toEqual( - `/management/policy/${hostDetails.metadata.Endpoint.policy.applied.id}` + `/policy/${hostDetails.metadata.Endpoint.policy.applied.id}` ); }); @@ -277,7 +277,7 @@ describe('when on the hosts page', () => { const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); expect(policyStatusLink).not.toBeNull(); expect(policyStatusLink.getAttribute('href')).toEqual( - '#/management/endpoints?page_index=0&page_size=10&selected_host=1&show=policy_response' + '/endpoints?page_index=0&page_size=10&selected_host=1&show=policy_response' ); }); @@ -489,7 +489,7 @@ describe('when on the hosts page', () => { const subHeaderBackLink = await renderResult.findByTestId('flyoutSubHeaderBackButton'); expect(subHeaderBackLink.textContent).toBe('Endpoint Details'); expect(subHeaderBackLink.getAttribute('href')).toBe( - '#/management/endpoints?page_index=0&page_size=10&selected_host=1' + '/endpoints?page_index=0&page_size=10&selected_host=1' ); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index 149819480855bd..45a33f76ee0c59 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -32,8 +32,14 @@ import { CreateStructuredSelector } from '../../../../common/store'; import { Immutable, HostInfo } from '../../../../../common/endpoint/types'; import { SpyRoute } from '../../../../common/utils/route/spy_routes'; import { ManagementPageView } from '../../../components/management_page_view'; -import { getManagementUrl } from '../../..'; import { FormattedDate } from '../../../../common/components/formatted_date'; +import { SecurityPageName } from '../../../../app/types'; +import { + getEndpointListPath, + getEndpointDetailsPath, + getPolicyDetailPath, +} from '../../../common/routing'; +import { useFormatUrl } from '../../../../common/components/link_to'; const HostListNavLink = memo<{ name: string; @@ -70,6 +76,7 @@ export const HostList = () => { uiQueryParams: queryParams, hasSelectedHost, } = useHostSelector(selector); + const { formatUrl, search } = useFormatUrl(SecurityPageName.management); const paginationSetup = useMemo(() => { return { @@ -86,9 +93,8 @@ export const HostList = () => { const { index, size } = page; // FIXME: PT: if host details is open, table is not displaying correct number of rows history.push( - getManagementUrl({ + getEndpointListPath({ name: 'endpointList', - excludePrefix: true, ...queryParams, page_index: JSON.stringify(index), page_size: JSON.stringify(size), @@ -110,17 +116,15 @@ export const HostList = () => { defaultMessage: 'Hostname', }), render: ({ hostname, id }: HostInfo['metadata']['host']) => { - const toRoutePath = getManagementUrl({ - ...queryParams, - name: 'endpointDetails', - selected_host: id, - excludePrefix: true, - }); - const toRouteUrl = getManagementUrl({ - ...queryParams, - name: 'endpointDetails', - selected_host: id, - }); + const toRoutePath = getEndpointDetailsPath( + { + ...queryParams, + name: 'endpointDetails', + selected_host: id, + }, + search + ); + const toRouteUrl = formatUrl(toRoutePath); return ( <HostListNavLink name={hostname} @@ -161,15 +165,8 @@ export const HostList = () => { truncateText: true, // eslint-disable-next-line react/display-name render: (policy: HostInfo['metadata']['Endpoint']['policy']['applied']) => { - const toRoutePath = getManagementUrl({ - name: 'policyDetails', - policyId: policy.id, - excludePrefix: true, - }); - const toRouteUrl = getManagementUrl({ - name: 'policyDetails', - policyId: policy.id, - }); + const toRoutePath = getPolicyDetailPath(policy.id); + const toRouteUrl = formatUrl(toRoutePath); return ( <HostListNavLink name={policy.name} @@ -187,15 +184,11 @@ export const HostList = () => { }), // eslint-disable-next-line react/display-name render: (policy: HostInfo['metadata']['Endpoint']['policy']['applied'], item: HostInfo) => { - const toRoutePath = getManagementUrl({ - name: 'endpointPolicyResponse', - selected_host: item.metadata.host.id, - excludePrefix: true, - }); - const toRouteUrl = getManagementUrl({ + const toRoutePath = getEndpointDetailsPath({ name: 'endpointPolicyResponse', selected_host: item.metadata.host.id, }); + const toRouteUrl = formatUrl(toRoutePath); return ( <EuiHealth color={POLICY_STATUS_TO_HEALTH_COLOR[policy.status]} @@ -257,7 +250,7 @@ export const HostList = () => { }, }, ]; - }, [queryParams]); + }, [formatUrl, queryParams, search]); return ( <ManagementPageView @@ -285,7 +278,7 @@ export const HostList = () => { pagination={paginationSetup} onChange={onTableChange} /> - <SpyRoute /> + <SpyRoute pageName={SecurityPageName.management} /> </ManagementPageView> ); }; diff --git a/x-pack/plugins/security_solution/public/management/pages/index.tsx b/x-pack/plugins/security_solution/public/management/pages/index.tsx index 588b2677632349..0e81b75d651baf 100644 --- a/x-pack/plugins/security_solution/public/management/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/index.tsx @@ -5,7 +5,8 @@ */ import React, { memo } from 'react'; -import { Redirect, Route, Switch } from 'react-router-dom'; +import { useHistory, Route, Switch } from 'react-router-dom'; + import { PolicyContainer } from './policy'; import { MANAGEMENT_ROUTING_ENDPOINTS_PATH, @@ -14,9 +15,10 @@ import { } from '../common/constants'; import { NotFoundPage } from '../../app/404'; import { EndpointsContainer } from './endpoint_hosts'; -import { getManagementUrl } from '..'; +import { getEndpointListPath } from '../common/routing'; export const ManagementContainer = memo(() => { + const history = useHistory(); return ( <Switch> <Route path={MANAGEMENT_ROUTING_ENDPOINTS_PATH} component={EndpointsContainer} /> @@ -24,9 +26,10 @@ export const ManagementContainer = memo(() => { <Route path={MANAGEMENT_ROUTING_ROOT_PATH} exact - render={() => ( - <Redirect to={getManagementUrl({ name: 'endpointList', excludePrefix: true })} /> - )} + render={() => { + history.replace(getEndpointListPath({ name: 'endpointList' })); + return null; + }} /> <Route path="*" component={NotFoundPage} /> </Switch> diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts index ce81c58893a7b2..c24c47becc0b53 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/index.test.ts @@ -26,10 +26,10 @@ import { createSpyMiddleware, MiddlewareActionSpyHelper, } from '../../../../../common/store/test_utils'; -import { getManagementUrl } from '../../../../common/routing'; +import { getPoliciesPath } from '../../../../common/routing'; describe('policy list store concerns', () => { - const policyListPathUrl = getManagementUrl({ name: 'policyList', excludePrefix: true }); + const policyListPathUrl = getPoliciesPath(); let fakeCoreStart: ReturnType<typeof coreMock.createStart>; let depsStart: DepsStartMock; let store: Store; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_datasource.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_datasource.tsx index 5e721cadfa8234..20346cb720acbc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_datasource.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_datasource.tsx @@ -13,7 +13,7 @@ import { CustomConfigureDatasourceContent, CustomConfigureDatasourceProps, } from '../../../../../../../ingest_manager/public'; -import { getManagementUrl } from '../../../..'; +import { getPolicyDetailPath } from '../../../../common/routing'; /** * Exports Endpoint-specific datasource configuration instructions @@ -24,10 +24,7 @@ export const ConfigureEndpointDatasource = memo<CustomConfigureDatasourceContent const { services } = useKibana(); let policyUrl = ''; if (from === 'edit' && datasourceId) { - policyUrl = getManagementUrl({ - name: 'policyDetails', - policyId: datasourceId, - }); + policyUrl = getPolicyDetailPath(datasourceId); } return ( diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx index 01e12e6c767a61..315e3d29b6df20 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx @@ -10,18 +10,13 @@ import { mount } from 'enzyme'; import { PolicyDetails } from './policy_details'; import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data'; import { createAppRootMockRenderer } from '../../../../common/mock/endpoint'; -import { getManagementUrl } from '../../../common/routing'; +import { getPolicyDetailPath, getPoliciesPath } from '../../../common/routing'; describe('Policy Details', () => { type FindReactWrapperResponse = ReturnType<ReturnType<typeof render>['find']>; - const policyDetailsPathUrl = getManagementUrl({ - name: 'policyDetails', - policyId: '1', - excludePrefix: true, - }); - const policyListPathUrl = getManagementUrl({ name: 'policyList', excludePrefix: true }); - const policyListPathUrlWithPrefix = getManagementUrl({ name: 'policyList' }); + const policyDetailsPathUrl = getPolicyDetailPath('1'); + const policyListPathUrl = getPoliciesPath(); const sleep = (ms = 100) => new Promise((wakeup) => setTimeout(wakeup, ms)); const generator = new EndpointDocGenerator(); const { history, AppWrapper, coreStart } = createAppRootMockRenderer(); @@ -97,7 +92,7 @@ describe('Policy Details', () => { const backToListButton = pageHeaderLeft.find('EuiButtonEmpty'); expect(backToListButton.prop('iconType')).toBe('arrowLeft'); - expect(backToListButton.prop('href')).toBe(policyListPathUrlWithPrefix); + expect(backToListButton.prop('href')).toBe(policyListPathUrl); expect(backToListButton.text()).toBe('Back to policy list'); const pageTitle = pageHeaderLeft.find('[data-test-subj="pageViewHeaderLeftTitle"]'); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx index d1f7da91bd6faf..6a48ae735180fb 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx @@ -38,11 +38,14 @@ import { useNavigateByRouterEventHandler } from '../../../../common/hooks/endpoi import { PageViewHeaderTitle } from '../../../../common/components/endpoint/page_view'; import { ManagementPageView } from '../../../components/management_page_view'; import { SpyRoute } from '../../../../common/utils/route/spy_routes'; -import { getManagementUrl } from '../../../common/routing'; +import { SecurityPageName } from '../../../../app/types'; +import { getPoliciesPath } from '../../../common/routing'; +import { useFormatUrl } from '../../../../common/components/link_to'; export const PolicyDetails = React.memo(() => { const dispatch = useDispatch<(action: AppAction) => void>(); const { notifications } = useKibana(); + const { formatUrl, search } = useFormatUrl(SecurityPageName.management); // Store values const policyItem = usePolicyDetailsSelector(policyDetails); @@ -89,9 +92,7 @@ export const PolicyDetails = React.memo(() => { } }, [notifications.toasts, policyName, policyUpdateStatus]); - const handleBackToListOnClick = useNavigateByRouterEventHandler( - getManagementUrl({ name: 'policyList', excludePrefix: true }) - ); + const handleBackToListOnClick = useNavigateByRouterEventHandler(getPoliciesPath()); const handleSaveOnClick = useCallback(() => { setShowConfirm(true); @@ -121,7 +122,7 @@ export const PolicyDetails = React.memo(() => { <span data-test-subj="policyDetailsIdNotFoundMessage">{policyApiError?.message}</span> </EuiCallOut> ) : null} - <SpyRoute /> + <SpyRoute pageName={SecurityPageName.management} /> </ManagementPageView> ); } @@ -133,7 +134,7 @@ export const PolicyDetails = React.memo(() => { iconType="arrowLeft" contentProps={{ style: { paddingLeft: '0' } }} onClick={handleBackToListOnClick} - href={getManagementUrl({ name: 'policyList' })} + href={formatUrl(getPoliciesPath(search))} > <FormattedMessage id="xpack.securitySolution.endpoint.policy.details.backToListTitle" @@ -226,7 +227,7 @@ export const PolicyDetails = React.memo(() => { <EuiSpacer size="l" /> <LinuxEvents /> </ManagementPageView> - <SpyRoute /> + <SpyRoute pageName={SecurityPageName.management} /> </> ); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx index 63a5cd0d67ae6f..acce5c8f783509 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx @@ -39,7 +39,7 @@ describe('when on the policies page', () => { let firstPolicyID: string; beforeEach(() => { reactTestingLibrary.act(() => { - history.push('/management/policy'); + history.push('/policy'); reactTestingLibrary.act(() => { const policyListData = mockPolicyResultList({ total: 3 }); firstPolicyID = policyListData.items[0].id; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx index 2d4abd6de0e420..4532408332d6e4 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx @@ -42,8 +42,10 @@ import { useNavigateByRouterEventHandler } from '../../../../common/hooks/endpoi import { LinkToApp } from '../../../../common/components/endpoint/link_to_app'; import { ManagementPageView } from '../../../components/management_page_view'; import { SpyRoute } from '../../../../common/utils/route/spy_routes'; -import { getManagementUrl } from '../../../common/routing'; import { FormattedDateAndTime } from '../../../../common/components/endpoint/formatted_date_time'; +import { SecurityPageName } from '../../../../app/types'; +import { useFormatUrl } from '../../../../common/components/link_to'; +import { getPolicyDetailPath, getPoliciesPath } from '../../../common/routing'; import { useNavigateToAppEventHandler } from '../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; import { CreateDatasourceRouteState } from '../../../../../../ingest_manager/public'; import { useEndpointPackageInfo } from './ingest_hooks'; @@ -127,6 +129,7 @@ export const PolicyList = React.memo(() => { const { services, notifications } = useKibana(); const history = useHistory(); const location = useLocation(); + const { formatUrl, search } = useFormatUrl(SecurityPageName.management); const [showDelete, setShowDelete] = useState<boolean>(false); const [policyIdToDelete, setPolicyIdToDelete] = useState<string>(''); @@ -155,14 +158,9 @@ export const PolicyList = React.memo(() => { // of the cancel and submit buttons and redirect the user back to endpoint policy path: `#/integrations${packageInfo ? `/endpoint-${packageInfo.version}/add-datasource` : ''}`, state: { - onCancelNavigateTo: [ - 'securitySolution', - { path: getManagementUrl({ name: 'policyList' }) }, - ], - onCancelUrl: services.application?.getUrlForApp('securitySolution', { - path: getManagementUrl({ name: 'policyList' }), - }), - onSaveNavigateTo: ['securitySolution', { path: getManagementUrl({ name: 'policyList' }) }], + onCancelNavigateTo: ['securitySolution:management', { path: getPoliciesPath() }], + onCancelUrl: formatUrl(getPoliciesPath()), + onSaveNavigateTo: ['securitySolution:management', { path: getPoliciesPath() }], }, } ); @@ -265,12 +263,8 @@ export const PolicyList = React.memo(() => { }), // eslint-disable-next-line react/display-name render: (name: string, item: Immutable<PolicyData>) => { - const routePath = getManagementUrl({ - name: 'policyDetails', - policyId: item.id, - excludePrefix: true, - }); - const routeUrl = getManagementUrl({ name: 'policyDetails', policyId: item.id }); + const routePath = getPolicyDetailPath(item.id, search); + const routeUrl = formatUrl(routePath); return ( <EuiFlexGroup gutterSize="s" alignItems="baseline" style={{ minWidth: 0 }}> <EuiFlexItem grow={false} style={NO_WRAP_TRUNCATE_STYLE}> @@ -382,7 +376,7 @@ export const PolicyList = React.memo(() => { ], }, ], - [services.application, handleDeleteOnClick] + [services.application, handleDeleteOnClick, formatUrl, search] ); return ( @@ -461,7 +455,7 @@ export const PolicyList = React.memo(() => { handleTableChange, paginationSetup, ])} - <SpyRoute /> + <SpyRoute pageName={SecurityPageName.management} /> </ManagementPageView> </> ); diff --git a/x-pack/plugins/security_solution/public/management/routes.tsx b/x-pack/plugins/security_solution/public/management/routes.tsx index 12727ea97458ed..209d7dd6dbcde4 100644 --- a/x-pack/plugins/security_solution/public/management/routes.tsx +++ b/x-pack/plugins/security_solution/public/management/routes.tsx @@ -5,14 +5,16 @@ */ import React from 'react'; -import { Route } from 'react-router-dom'; +import { Route, Switch } from 'react-router-dom'; import { ManagementContainer } from './pages'; -import { MANAGEMENT_ROUTING_ROOT_PATH } from './common/constants'; +import { NotFoundPage } from '../app/404'; /** * Returns the React Router Routes for the management area */ -export const managementRoutes = () => [ - // Mounts the Management interface on `/management` - <Route path={MANAGEMENT_ROUTING_ROOT_PATH} component={ManagementContainer} />, -]; +export const ManagementRoutes = () => ( + <Switch> + <Route path="/" component={ManagementContainer} /> + <Route render={() => <NotFoundPage />} /> + </Switch> +); diff --git a/x-pack/plugins/security_solution/public/management/types.ts b/x-pack/plugins/security_solution/public/management/types.ts index fd6b9f6e6a8245..854e9faa0204de 100644 --- a/x-pack/plugins/security_solution/public/management/types.ts +++ b/x-pack/plugins/security_solution/public/management/types.ts @@ -5,7 +5,7 @@ */ import { CombinedState } from 'redux'; -import { SiemPageName } from '../app/types'; +import { SecurityPageName } from '../app/types'; import { PolicyListState, PolicyDetailsState } from './pages/policy/types'; import { HostState } from './pages/endpoint_hosts/types'; @@ -33,7 +33,7 @@ export enum ManagementSubTab { * The URL route params for the Management Policy List section */ export interface ManagementRoutePolicyListParams { - pageName: SiemPageName.management; + pageName: SecurityPageName.management; tabName: ManagementSubTab.policies; } diff --git a/x-pack/plugins/security_solution/public/network/components/ip/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/ip/index.test.tsx index 8d53e75b81afc8..d9496102648831 100644 --- a/x-pack/plugins/security_solution/public/network/components/ip/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/ip/index.test.tsx @@ -12,6 +12,8 @@ import { useMountAppended } from '../../../common/utils/use_mount_appended'; import { Ip } from '.'; +jest.mock('../../../common/components/link_to'); + describe('Port', () => { const mount = useMountAppended(); @@ -42,6 +44,6 @@ describe('Port', () => { expect( wrapper.find('[data-test-subj="draggable-content-destination.ip"]').find('a').first().props() .href - ).toEqual('#/link-to/network/ip/10.1.2.3/source'); + ).toEqual('/ip/10.1.2.3/source'); }); }); diff --git a/x-pack/plugins/security_solution/public/network/components/network_http_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/network_http_table/index.test.tsx index 39fad58ca3528b..ac37aaf3091552 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_http_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_http_table/index.test.tsx @@ -24,6 +24,8 @@ import { networkModel } from '../../store'; import { NetworkHttpTable } from '.'; import { mockData } from './mock'; +jest.mock('../../../common/components/link_to'); + describe('NetworkHttp Table Component', () => { const loadPage = jest.fn(); const state: State = mockGlobalState; diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.test.tsx index 4de04f673d879f..b14d411810dee0 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.test.tsx @@ -24,6 +24,8 @@ import { networkModel } from '../../store'; import { NetworkTopNFlowTable } from '.'; import { mockData } from './mock'; +jest.mock('../../../common/components/link_to'); + describe('NetworkTopNFlow Table Component', () => { const loadPage = jest.fn(); const state: State = mockGlobalState; diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx index 3016e34dfd73c4..570e20dc5656dd 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/index.test.tsx @@ -95,6 +95,18 @@ const getSourceDestinationInstance = () => ( /> ); +jest.mock('react-router-dom', () => { + const original = jest.requireActual('react-router-dom'); + + return { + ...original, + useHistory: () => ({ + createHref: jest.fn(), + push: jest.fn(), + }), + }; +}); + describe('SourceDestination', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx index 3c1668bbb3419d..d9d926fb52d10e 100644 --- a/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/source_destination/source_destination_ip.test.tsx @@ -35,6 +35,8 @@ import { SOURCE_GEO_REGION_NAME_FIELD_NAME, } from './geo_fields'; +jest.mock('../../../common/components/link_to'); + describe('SourceDestinationIp', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/security_solution/public/network/index.ts b/x-pack/plugins/security_solution/public/network/index.ts index 63291ad2d2396a..bd8148f339d690 100644 --- a/x-pack/plugins/security_solution/public/network/index.ts +++ b/x-pack/plugins/security_solution/public/network/index.ts @@ -6,7 +6,7 @@ import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { SecuritySubPluginWithStore } from '../app/types'; -import { getNetworkRoutes } from './routes'; +import { NetworkRoutes } from './routes'; import { initialNetworkState, networkReducer, NetworkState } from './store'; import { TimelineId } from '../../common/types/timeline'; import { getTimelinesInStorageByIds } from '../timelines/containers/local_storage'; @@ -16,7 +16,7 @@ export class Network { public start(storage: Storage): SecuritySubPluginWithStore<'network', NetworkState> { return { - routes: getNetworkRoutes(), + SubPluginRoutes: NetworkRoutes, storageTimelines: { timelineById: getTimelinesInStorageByIds(storage, [TimelineId.networkPageExternalAlerts]), }, diff --git a/x-pack/plugins/security_solution/public/network/pages/index.tsx b/x-pack/plugins/security_solution/public/network/pages/index.tsx index c6f13c118c3093..c7a8a5f705dfe6 100644 --- a/x-pack/plugins/security_solution/public/network/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/index.tsx @@ -5,7 +5,7 @@ */ import React, { useMemo } from 'react'; -import { Redirect, Route, Switch, RouteComponentProps } from 'react-router-dom'; +import { Route, Switch, RouteComponentProps, useHistory } from 'react-router-dom'; import { useMlCapabilities } from '../../common/components/ml_popover/hooks/use_ml_capabilities'; import { hasMlUserPermissions } from '../../../common/machine_learning/has_ml_user_permissions'; @@ -14,23 +14,24 @@ import { FlowTarget } from '../../graphql/types'; import { IPDetails } from './ip_details'; import { Network } from './network'; import { GlobalTime } from '../../common/containers/global_time'; -import { SiemPageName } from '../../app/types'; import { getNetworkRoutePath } from './navigation'; import { NetworkRouteType } from './navigation/types'; +import { MlNetworkConditionalContainer } from '../../common/components/ml/conditional_links/ml_network_conditional_container'; type Props = Partial<RouteComponentProps<{}>> & { url: string }; -const networkPagePath = `/:pageName(${SiemPageName.network})`; -const ipDetailsPageBasePath = `${networkPagePath}/ip/:detailName`; +const networkPagePath = ''; +const ipDetailsPageBasePath = `/ip/:detailName`; const NetworkContainerComponent: React.FC<Props> = () => { + const history = useHistory(); const capabilities = useMlCapabilities(); const capabilitiesFetched = capabilities.capabilitiesFetched; const userHasMlUserPermissions = useMemo(() => hasMlUserPermissions(capabilities), [ capabilities, ]); const networkRoutePath = useMemo( - () => getNetworkRoutePath(networkPagePath, capabilitiesFetched, userHasMlUserPermissions), + () => getNetworkRoutePath(capabilitiesFetched, userHasMlUserPermissions), [capabilitiesFetched, userHasMlUserPermissions] ); @@ -38,6 +39,12 @@ const NetworkContainerComponent: React.FC<Props> = () => { <GlobalTime> {({ to, from, setQuery, deleteQuery, isInitializing }) => ( <Switch> + <Route + path="/ml-network" + render={({ location, match }) => ( + <MlNetworkConditionalContainer location={location} url={match.url} /> + )} + /> <Route strict path={networkRoutePath} @@ -79,17 +86,17 @@ const NetworkContainerComponent: React.FC<Props> = () => { match: { params: { detailName }, }, - }) => ( - <Redirect - to={`/${SiemPageName.network}/ip/${detailName}/${FlowTarget.source}${search}`} - /> - )} + }) => { + history.replace(`ip/${detailName}/${FlowTarget.source}${search}`); + return null; + }} /> <Route - path={`/${SiemPageName.network}/`} - render={({ location: { search = '' } }) => ( - <Redirect to={`/${SiemPageName.network}/${NetworkRouteType.flows}${search}`} /> - )} + path="/" + render={({ location: { search = '' } }) => { + history.replace(`${NetworkRouteType.flows}${search}`); + return null; + }} /> </Switch> )} diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/network/pages/ip_details/__snapshots__/index.test.tsx.snap index e7598ef03d7865..6e76ff00a8141c 100644 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/pages/ip_details/__snapshots__/index.test.tsx.snap @@ -8,6 +8,8 @@ exports[`Ip Details it matches the snapshot 1`] = ` > <Component /> </WithSource> - <withRouter(undefined) /> + <withRouter(undefined) + pageName="network" + /> </Fragment> `; diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx b/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx index 9ae09d6c6cec74..face3f8904794e 100644 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx @@ -45,6 +45,7 @@ import { UsersQueryTable } from './users_query_table'; import { AnomaliesQueryTabBody } from '../../../common/containers/anomalies/anomalies_query_tab_body'; import { esQuery } from '../../../../../../../src/plugins/data/public'; import { networkModel } from '../../store'; +import { SecurityPageName } from '../../../app/types'; export { getBreadcrumbs } from './utils'; const IpOverviewManage = manageQuery(IpOverview); @@ -273,7 +274,7 @@ export const IPDetailsComponent: React.FC<IPDetailsComponentProps & PropsFromRed }} </WithSource> - <SpyRoute /> + <SpyRoute pageName={SecurityPageName.network} /> </> ); }; diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/utils.ts b/x-pack/plugins/security_solution/public/network/pages/ip_details/utils.ts index b1f986f20778f2..640b9d9818cdd1 100644 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/utils.ts +++ b/x-pack/plugins/security_solution/public/network/pages/ip_details/utils.ts @@ -9,14 +9,14 @@ import { get, isEmpty } from 'lodash/fp'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ChromeBreadcrumb } from '../../../../../../../src/core/public'; import { decodeIpv6 } from '../../../common/lib/helpers'; -import { - getNetworkUrl, - getIPDetailsUrl, -} from '../../../common/components/link_to/redirect_to_network'; +import { getIPDetailsUrl } from '../../../common/components/link_to/redirect_to_network'; import { networkModel } from '../../store'; import * as i18n from '../translations'; import { NetworkRouteType } from '../navigation/types'; import { NetworkRouteSpyState } from '../../../common/utils/route/types'; +import { GetUrlForApp } from '../../../common/components/navigation/types'; +import { APP_ID } from '../../../../common/constants'; +import { SecurityPageName } from '../../../app/types'; export const type = networkModel.NetworkType.details; const TabNameMappedToI18nKey: Record<NetworkRouteType, string> = { @@ -30,12 +30,15 @@ const TabNameMappedToI18nKey: Record<NetworkRouteType, string> = { export const getBreadcrumbs = ( params: NetworkRouteSpyState, - search: string[] + search: string[], + getUrlForApp: GetUrlForApp ): ChromeBreadcrumb[] => { let breadcrumb = [ { text: i18n.PAGE_TITLE, - href: `${getNetworkUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, + href: getUrlForApp(`${APP_ID}:${SecurityPageName.network}`, { + path: !isEmpty(search[0]) ? search[0] : '', + }), }, ]; if (params.detailName != null) { @@ -43,9 +46,13 @@ export const getBreadcrumbs = ( ...breadcrumb, { text: decodeIpv6(params.detailName), - href: `${getIPDetailsUrl(params.detailName, params.flowTarget)}${ - !isEmpty(search[1]) ? search[1] : '' - }`, + href: getUrlForApp(`${APP_ID}:${SecurityPageName.network}`, { + path: getIPDetailsUrl( + params.detailName, + params.flowTarget, + !isEmpty(search[0]) ? search[0] : '' + ), + }), }, ]; } diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/nav_tabs.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/nav_tabs.tsx index 61f1a5aacb9c0c..a563d44012ea23 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/nav_tabs.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/nav_tabs.tsx @@ -7,8 +7,9 @@ import { omit } from 'lodash/fp'; import * as i18n from '../translations'; import { NetworkNavTab, NetworkRouteType } from './types'; +import { SecurityPageName } from '../../../app/types'; -const getTabsOnNetworkUrl = (tabName: NetworkRouteType) => `#/network/${tabName}`; +const getTabsOnNetworkUrl = (tabName: NetworkRouteType) => `/${tabName}`; export const navTabsNetwork = (hasMlUserPermissions: boolean): NetworkNavTab => { const networkNavTabs = { @@ -18,6 +19,7 @@ export const navTabsNetwork = (hasMlUserPermissions: boolean): NetworkNavTab => href: getTabsOnNetworkUrl(NetworkRouteType.flows), disabled: false, urlKey: 'network', + pageId: SecurityPageName.network, }, [NetworkRouteType.dns]: { id: NetworkRouteType.dns, @@ -25,6 +27,7 @@ export const navTabsNetwork = (hasMlUserPermissions: boolean): NetworkNavTab => href: getTabsOnNetworkUrl(NetworkRouteType.dns), disabled: false, urlKey: 'network', + pageId: SecurityPageName.network, }, [NetworkRouteType.http]: { id: NetworkRouteType.http, @@ -32,6 +35,7 @@ export const navTabsNetwork = (hasMlUserPermissions: boolean): NetworkNavTab => href: getTabsOnNetworkUrl(NetworkRouteType.http), disabled: false, urlKey: 'network', + pageId: SecurityPageName.network, }, [NetworkRouteType.tls]: { id: NetworkRouteType.tls, @@ -39,6 +43,7 @@ export const navTabsNetwork = (hasMlUserPermissions: boolean): NetworkNavTab => href: getTabsOnNetworkUrl(NetworkRouteType.tls), disabled: false, urlKey: 'network', + pageId: SecurityPageName.network, }, [NetworkRouteType.anomalies]: { id: NetworkRouteType.anomalies, @@ -46,6 +51,7 @@ export const navTabsNetwork = (hasMlUserPermissions: boolean): NetworkNavTab => href: getTabsOnNetworkUrl(NetworkRouteType.anomalies), disabled: false, urlKey: 'network', + pageId: SecurityPageName.network, }, [NetworkRouteType.alerts]: { id: NetworkRouteType.alerts, @@ -53,6 +59,7 @@ export const navTabsNetwork = (hasMlUserPermissions: boolean): NetworkNavTab => href: getTabsOnNetworkUrl(NetworkRouteType.alerts), disabled: false, urlKey: 'network', + pageId: SecurityPageName.network, }, }; diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/network_routes.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/network_routes.tsx index 08ed0d9769be8a..e1e84b3ef4271f 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/network_routes.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/network_routes.tsx @@ -100,10 +100,10 @@ export const NetworkRoutes = React.memo<NetworkRoutesProps>( return ( <Switch> - <Route path={`${networkPagePath}/:tabName(${NetworkRouteType.dns})`}> + <Route path={`/:tabName(${NetworkRouteType.dns})`}> <DnsQueryTabBody {...tabProps} /> </Route> - <Route path={`${networkPagePath}/:tabName(${NetworkRouteType.flows})`}> + <Route path={`/:tabName(${NetworkRouteType.flows})`}> <> <ConditionalFlexGroup direction="column"> <EuiFlexItem> @@ -129,19 +129,19 @@ export const NetworkRoutes = React.memo<NetworkRoutesProps>( </ConditionalFlexGroup> </> </Route> - <Route path={`${networkPagePath}/:tabName(${NetworkRouteType.http})`}> + <Route path={`/:tabName(${NetworkRouteType.http})`}> <HttpQueryTabBody {...tabProps} /> </Route> - <Route path={`${networkPagePath}/:tabName(${NetworkRouteType.tls})`}> + <Route path={`/:tabName(${NetworkRouteType.tls})`}> <TlsQueryTabBody {...tabProps} flowTarget={FlowTargetSourceDest.source} /> </Route> - <Route path={`${networkPagePath}/:tabName(${NetworkRouteType.anomalies})`}> + <Route path={`/:tabName(${NetworkRouteType.anomalies})`}> <AnomaliesQueryTabBody {...anomaliesProps} AnomaliesTableComponent={AnomaliesNetworkTable} /> </Route> - <Route path={`${networkPagePath}/:tabName(${NetworkRouteType.alerts})`}> + <Route path={`/:tabName(${NetworkRouteType.alerts})`}> <NetworkAlertsQueryTabBody {...tabProps} /> </Route> </Switch> diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts b/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts index 0f48aad57b3a84..433ed7fffd7419 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts @@ -67,11 +67,10 @@ export enum NetworkRouteType { anomalies = 'anomalies', tls = 'tls', http = 'http', - alerts = 'alerts', + alerts = 'external-alerts', } export type GetNetworkRoutePath = ( - pagePath: string, capabilitiesFetched: boolean, hasMlUserPermission: boolean ) => string; diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/utils.ts b/x-pack/plugins/security_solution/public/network/pages/navigation/utils.ts index 24c2011fd38007..25a91db4eef9b6 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/utils.ts +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/utils.ts @@ -7,16 +7,15 @@ import { GetNetworkRoutePath, NetworkRouteType } from './types'; export const getNetworkRoutePath: GetNetworkRoutePath = ( - pagePath, capabilitiesFetched, hasMlUserPermission ) => { if (capabilitiesFetched && !hasMlUserPermission) { - return `${pagePath}/:tabName(${NetworkRouteType.flows}|${NetworkRouteType.dns}|${NetworkRouteType.http}|${NetworkRouteType.tls}|${NetworkRouteType.alerts})`; + return `/:tabName(${NetworkRouteType.flows}|${NetworkRouteType.dns}|${NetworkRouteType.http}|${NetworkRouteType.tls}|${NetworkRouteType.alerts})`; } return ( - `${pagePath}/:tabName(` + + `/:tabName(` + `${NetworkRouteType.flows}|` + `${NetworkRouteType.dns}|` + `${NetworkRouteType.anomalies}|` + diff --git a/x-pack/plugins/security_solution/public/network/pages/network.tsx b/x-pack/plugins/security_solution/public/network/pages/network.tsx index 2f7a97ed3d19ef..845a6bbd95dd6d 100644 --- a/x-pack/plugins/security_solution/public/network/pages/network.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/network.tsx @@ -11,6 +11,7 @@ import { useParams } from 'react-router-dom'; import { StickyContainer } from 'react-sticky'; import { esQuery } from '../../../../../../src/plugins/data/public'; +import { SecurityPageName } from '../../app/types'; import { UpdateDateRange } from '../../common/components/charts/common'; import { EmbeddedMap } from '../components/embeddables/embedded_map'; import { FiltersGlobal } from '../../common/components/filters_global'; @@ -175,7 +176,7 @@ const NetworkComponent = React.memo<NetworkComponentProps & PropsFromRedux>( }} </WithSource> - <SpyRoute /> + <SpyRoute pageName={SecurityPageName.network} /> </> ); } diff --git a/x-pack/plugins/security_solution/public/network/routes.tsx b/x-pack/plugins/security_solution/public/network/routes.tsx index 6f3fd28ec53b7b..d6d725512bdb77 100644 --- a/x-pack/plugins/security_solution/public/network/routes.tsx +++ b/x-pack/plugins/security_solution/public/network/routes.tsx @@ -5,14 +5,17 @@ */ import React from 'react'; -import { Route } from 'react-router-dom'; +import { Route, Switch } from 'react-router-dom'; import { NetworkContainer } from './pages'; -import { SiemPageName } from '../app/types'; +import { NotFoundPage } from '../app/404'; -export const getNetworkRoutes = () => [ - <Route - path={`/:pageName(${SiemPageName.network})`} - render={({ location, match }) => <NetworkContainer location={location} url={match.url} />} - />, -]; +export const NetworkRoutes = () => ( + <Switch> + <Route + path="/" + render={({ location, match }) => <NetworkContainer location={location} url={match.url} />} + /> + <Route render={() => <NotFoundPage />} /> + </Switch> +); diff --git a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx index ba82c7f92855d7..1aa114608b479c 100644 --- a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx @@ -17,8 +17,8 @@ import { mockIndexPattern, TestProviders } from '../../../common/mock'; import { AlertsByCategory } from '.'; +jest.mock('../../../common/components/link_to'); jest.mock('../../../common/lib/kibana'); - jest.mock('../../../common/containers/matrix_histogram', () => { return { useQuery: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx index fe66ae2624c2ae..03e8279f01db70 100644 --- a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx @@ -4,12 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButton } from '@elastic/eui'; import numeral from '@elastic/numeral'; -import React, { useEffect, useMemo } from 'react'; +import React, { useEffect, useMemo, useCallback } from 'react'; import { Position } from '@elastic/charts'; -import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; +import { DEFAULT_NUMBER_FORMAT, APP_ID } from '../../../../common/constants'; import { SHOWING, UNIT } from '../../../common/components/alerts_viewer/translations'; import { MatrixHistogramContainer } from '../../../common/components/matrix_histogram'; import { useKibana, useUiSetting$ } from '../../../common/lib/kibana'; @@ -29,9 +28,10 @@ import { histogramConfigs, } from '../../../common/components/alerts_viewer/histogram_configs'; import { MatrixHisrogramConfigs } from '../../../common/components/matrix_histogram/types'; -import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; -import { navTabs } from '../../../app/home/home_navigations'; import { getTabsOnHostsUrl } from '../../../common/components/link_to/redirect_to_hosts'; +import { SecurityPageName } from '../../../app/types'; +import { useFormatUrl } from '../../../common/components/link_to'; +import { LinkButton } from '../../../common/components/links'; const ID = 'alertsByCategoryOverview'; @@ -75,19 +75,31 @@ const AlertsByCategoryComponent: React.FC<Props> = ({ }, []); const kibana = useKibana(); + const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.hosts); + const { navigateToApp } = kibana.services.application; const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); - const urlSearch = useGetUrlSearch(navTabs.hosts); + + const goToHostAlerts = useCallback( + (ev) => { + ev.preventDefault(); + navigateToApp(`${APP_ID}:${SecurityPageName.hosts}`, { + path: getTabsOnHostsUrl(HostsTableType.alerts, urlSearch), + }); + }, + [navigateToApp, urlSearch] + ); const alertsCountViewAlertsButton = useMemo( () => ( - <EuiButton + <LinkButton data-test-subj="view-alerts" - href={getTabsOnHostsUrl(HostsTableType.alerts, urlSearch)} + onClick={goToHostAlerts} + href={formatUrl(getTabsOnHostsUrl(HostsTableType.alerts))} > {i18n.VIEW_ALERTS} - </EuiButton> + </LinkButton> ), - [urlSearch] + [goToHostAlerts, formatUrl] ); const alertsByCategoryHistogramConfigs: MatrixHisrogramConfigs = useMemo( diff --git a/x-pack/plugins/security_solution/public/overview/components/event_counts/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/event_counts/index.test.tsx index 49fe609e0cf91a..95dd65f559470d 100644 --- a/x-pack/plugins/security_solution/public/overview/components/event_counts/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/event_counts/index.test.tsx @@ -13,6 +13,8 @@ import { mockIndexPattern, TestProviders } from '../../../common/mock'; import { EventCounts } from '.'; +jest.mock('../../../common/components/link_to'); + describe('EventCounts', () => { const from = 1579553397080; const to = 1579639797080; diff --git a/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx b/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx index 0065e5a61b3ffc..fe3f9f8ecda332 100644 --- a/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx @@ -5,12 +5,11 @@ */ import { Position } from '@elastic/charts'; -import { EuiButton } from '@elastic/eui'; import numeral from '@elastic/numeral'; -import React, { useEffect, useMemo } from 'react'; +import React, { useEffect, useMemo, useCallback } from 'react'; import uuid from 'uuid'; -import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; +import { DEFAULT_NUMBER_FORMAT, APP_ID } from '../../../../common/constants'; import { SHOWING, UNIT } from '../../../common/components/events_viewer/translations'; import { getTabsOnHostsUrl } from '../../../common/components/link_to/redirect_to_hosts'; import { MatrixHistogramContainer } from '../../../common/components/matrix_histogram'; @@ -18,8 +17,6 @@ import { MatrixHisrogramConfigs, MatrixHistogramOption, } from '../../../common/components/matrix_histogram/types'; -import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; -import { navTabs } from '../../../app/home/home_navigations'; import { eventsStackByOptions } from '../../../hosts/pages/navigation'; import { convertToBuildEsQuery } from '../../../common/lib/keury'; import { useKibana, useUiSetting$ } from '../../../common/lib/kibana'; @@ -35,6 +32,9 @@ import { HostsTableType, HostsType } from '../../../hosts/store/model'; import { InputsModelId } from '../../../common/store/inputs/constants'; import * as i18n from '../../pages/translations'; +import { SecurityPageName } from '../../../app/types'; +import { useFormatUrl } from '../../../common/components/link_to'; +import { LinkButton } from '../../../common/components/links'; const NO_FILTERS: Filter[] = []; const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; @@ -95,16 +95,30 @@ const EventsByDatasetComponent: React.FC<Props> = ({ }, [deleteQuery, uniqueQueryId]); const kibana = useKibana(); + const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.hosts); + const { navigateToApp } = kibana.services.application; const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); - const urlSearch = useGetUrlSearch(navTabs.hosts); + + const goToHostEvents = useCallback( + (ev) => { + ev.preventDefault(); + navigateToApp(`${APP_ID}:${SecurityPageName.hosts}`, { + path: getTabsOnHostsUrl(HostsTableType.events, urlSearch), + }); + }, + [navigateToApp, urlSearch] + ); const eventsCountViewEventsButton = useMemo( () => ( - <EuiButton href={getTabsOnHostsUrl(HostsTableType.events, urlSearch)}> + <LinkButton + onClick={goToHostEvents} + href={formatUrl(getTabsOnHostsUrl(HostsTableType.events))} + > {i18n.VIEW_EVENTS} - </EuiButton> + </LinkButton> ), - [urlSearch] + [goToHostEvents, formatUrl] ); const filterQuery = useMemo( diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx index 8b04c329731ab8..d29efa2d44c15c 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx @@ -25,6 +25,7 @@ import { GetOverviewHostQuery } from '../../../graphql/types'; import { wait } from '../../../common/lib/helpers'; jest.mock('../../../common/lib/kibana'); +jest.mock('../../../common/components/link_to'); const startDate = 1579553397080; const endDate = 1579639797080; diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx index 4fb37a99792451..195bb4fa0807a0 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx @@ -5,23 +5,23 @@ */ import { isEmpty } from 'lodash/fp'; -import { EuiButton, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import { EuiFlexItem, EuiPanel } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useMemo } from 'react'; +import React, { useMemo, useCallback } from 'react'; -import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; +import { DEFAULT_NUMBER_FORMAT, APP_ID } from '../../../../common/constants'; import { ESQuery } from '../../../../common/typed_json'; import { ID as OverviewHostQueryId, OverviewHostQuery } from '../../containers/overview_host'; import { HeaderSection } from '../../../common/components/header_section'; -import { useUiSetting$ } from '../../../common/lib/kibana'; -import { getHostsUrl } from '../../../common/components/link_to'; +import { useUiSetting$, useKibana } from '../../../common/lib/kibana'; +import { getHostsUrl, useFormatUrl } from '../../../common/components/link_to'; import { getOverviewHostStats, OverviewHostStats } from '../overview_host_stats'; import { manageQuery } from '../../../common/components/page/manage_query'; import { inputsModel } from '../../../common/store/inputs'; import { InspectButtonContainer } from '../../../common/components/inspect'; -import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; -import { navTabs } from '../../../app/home/home_navigations'; +import { SecurityPageName } from '../../../app/types'; +import { LinkButton } from '../../../common/components/links'; export interface OwnProps { startDate: number; @@ -49,18 +49,30 @@ const OverviewHostComponent: React.FC<OverviewHostProps> = ({ startDate, setQuery, }) => { + const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.hosts); + const { navigateToApp } = useKibana().services.application; const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); - const urlSearch = useGetUrlSearch(navTabs.hosts); + + const goToHost = useCallback( + (ev) => { + ev.preventDefault(); + navigateToApp(`${APP_ID}:${SecurityPageName.hosts}`, { + path: getHostsUrl(urlSearch), + }); + }, + [navigateToApp, urlSearch] + ); + const hostPageButton = useMemo( () => ( - <EuiButton href={getHostsUrl(urlSearch)}> + <LinkButton onClick={goToHost} href={formatUrl(getHostsUrl())}> <FormattedMessage id="xpack.securitySolution.overview.hostsAction" defaultMessage="View hosts" /> - </EuiButton> + </LinkButton> ), - [urlSearch] + [goToHost, formatUrl] ); return ( <EuiFlexItem> diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx index 2fcf6f83f0ae0e..b4b685465dbda4 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx @@ -22,6 +22,7 @@ import { overviewNetworkQuery } from '../../containers/overview_network/index.gq import { GetOverviewHostQuery } from '../../../graphql/types'; import { wait } from '../../../common/lib/helpers'; +jest.mock('../../../common/components/link_to'); jest.mock('../../../common/lib/kibana'); const startDate = 1579553397080; diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx index 94a3c13e399474..31544eaa2d3b0a 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx @@ -5,15 +5,15 @@ */ import { isEmpty } from 'lodash/fp'; -import { EuiButton, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import { EuiFlexItem, EuiPanel } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useMemo } from 'react'; +import React, { useMemo, useCallback } from 'react'; -import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; +import { DEFAULT_NUMBER_FORMAT, APP_ID } from '../../../../common/constants'; import { ESQuery } from '../../../../common/typed_json'; import { HeaderSection } from '../../../common/components/header_section'; -import { useUiSetting$ } from '../../../common/lib/kibana'; +import { useUiSetting$, useKibana } from '../../../common/lib/kibana'; import { manageQuery } from '../../../common/components/page/manage_query'; import { ID as OverviewNetworkQueryId, @@ -21,10 +21,10 @@ import { } from '../../containers/overview_network'; import { inputsModel } from '../../../common/store/inputs'; import { getOverviewNetworkStats, OverviewNetworkStats } from '../overview_network_stats'; -import { getNetworkUrl } from '../../../common/components/link_to'; +import { getNetworkUrl, useFormatUrl } from '../../../common/components/link_to'; import { InspectButtonContainer } from '../../../common/components/inspect'; -import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; -import { navTabs } from '../../../app/home/home_navigations'; +import { SecurityPageName } from '../../../app/types'; +import { LinkButton } from '../../../common/components/links'; export interface OverviewNetworkProps { startDate: number; @@ -51,19 +51,32 @@ const OverviewNetworkComponent: React.FC<OverviewNetworkProps> = ({ startDate, setQuery, }) => { + const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.hosts); + const { navigateToApp } = useKibana().services.application; const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT); - const urlSearch = useGetUrlSearch(navTabs.network); + + const goToNetwork = useCallback( + (ev) => { + ev.preventDefault(); + navigateToApp(`${APP_ID}:${SecurityPageName.hosts}`, { + path: getNetworkUrl(urlSearch), + }); + }, + [navigateToApp, urlSearch] + ); + const networkPageButton = useMemo( () => ( - <EuiButton href={getNetworkUrl(urlSearch)}> + <LinkButton onClick={goToNetwork} href={formatUrl(getNetworkUrl())}> <FormattedMessage id="xpack.securitySolution.overview.networkAction" defaultMessage="View network" /> - </EuiButton> + </LinkButton> ), - [urlSearch] + [goToNetwork, formatUrl] ); + return ( <EuiFlexItem> <InspectButtonContainer> diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_cases/index.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_cases/index.tsx index 03c1754f1b8d5a..2618ad950a77ad 100644 --- a/x-pack/plugins/security_solution/public/overview/components/recent_cases/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/recent_cases/index.tsx @@ -4,18 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiHorizontalRule, EuiLink, EuiText } from '@elastic/eui'; -import React, { useEffect, useMemo, useRef } from 'react'; +import { EuiHorizontalRule, EuiText } from '@elastic/eui'; +import React, { useEffect, useMemo, useRef, useCallback } from 'react'; import { FilterOptions, QueryParams } from '../../../cases/containers/types'; import { DEFAULT_QUERY_PARAMS, useGetCases } from '../../../cases/containers/use_get_cases'; -import { getCaseUrl } from '../../../common/components/link_to/redirect_to_case'; -import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; -import { navTabs } from '../../../app/home/home_navigations'; import { LoadingPlaceholders } from '../loading_placeholders'; import { NoCases } from './no_cases'; import { RecentCases } from './recent_cases'; import * as i18n from './translations'; +import { useKibana } from '../../../common/lib/kibana'; +import { APP_ID } from '../../../../common/constants'; +import { SecurityPageName } from '../../../app/types'; +import { useFormatUrl } from '../../../common/components/link_to'; +import { LinkAnchor } from '../../../common/components/links'; const usePrevious = (value: FilterOptions) => { const ref = useRef(); @@ -34,16 +36,31 @@ const queryParams: QueryParams = { const StatefulRecentCasesComponent = React.memo( ({ filterOptions }: { filterOptions: FilterOptions }) => { + const { formatUrl } = useFormatUrl(SecurityPageName.case); + const { navigateToApp } = useKibana().services.application; const previousFilterOptions = usePrevious(filterOptions); const { data, loading, setFilters } = useGetCases(queryParams); const isLoadingCases = useMemo( () => loading.indexOf('cases') > -1 || loading.indexOf('caseUpdate') > -1, [loading] ); - const search = useGetUrlSearch(navTabs.case); + + const goToCases = useCallback( + (ev) => { + ev.preventDefault(); + navigateToApp(`${APP_ID}:${SecurityPageName.case}`); + }, + [navigateToApp] + ); + const allCasesLink = useMemo( - () => <EuiLink href={getCaseUrl(search)}>{i18n.VIEW_ALL_CASES}</EuiLink>, - [search] + () => ( + <LinkAnchor onClick={goToCases} href={formatUrl('')}> + {' '} + {i18n.VIEW_ALL_CASES} + </LinkAnchor> + ), + [goToCases, formatUrl] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.tsx index e29223ca07e65e..40969a6e1df4a6 100644 --- a/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/recent_cases/no_cases/index.tsx @@ -4,20 +4,37 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiLink } from '@elastic/eui'; -import React, { useMemo } from 'react'; +import React, { useMemo, useCallback } from 'react'; +import { APP_ID } from '../../../../../common/constants'; import { getCreateCaseUrl } from '../../../../common/components/link_to/redirect_to_case'; -import { useGetUrlSearch } from '../../../../common/components/navigation/use_get_url_search'; -import { navTabs } from '../../../../app/home/home_navigations'; - +import { LinkAnchor } from '../../../../common/components/links'; +import { useFormatUrl } from '../../../../common/components/link_to'; import * as i18n from '../translations'; +import { useKibana } from '../../../../common/lib/kibana'; +import { SecurityPageName } from '../../../../app/types'; const NoCasesComponent = () => { - const urlSearch = useGetUrlSearch(navTabs.case); + const { formatUrl, search } = useFormatUrl(SecurityPageName.case); + const { navigateToApp } = useKibana().services.application; + + const goToCreateCase = useCallback( + (ev) => { + ev.preventDefault(); + navigateToApp(`${APP_ID}:${SecurityPageName.hosts}`, { + path: getCreateCaseUrl(search), + }); + }, + [navigateToApp, search] + ); const newCaseLink = useMemo( - () => <EuiLink href={getCreateCaseUrl(urlSearch)}>{` ${i18n.START_A_NEW_CASE}`}</EuiLink>, - [urlSearch] + () => ( + <LinkAnchor + onClick={goToCreateCase} + href={formatUrl(getCreateCaseUrl())} + >{` ${i18n.START_A_NEW_CASE}`}</LinkAnchor> + ), + [formatUrl, goToCreateCase] ); return ( diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_cases/recent_cases.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_cases/recent_cases.tsx index 9618ddb05716d0..5867a9d859f04c 100644 --- a/x-pack/plugins/security_solution/public/overview/components/recent_cases/recent_cases.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/recent_cases/recent_cases.tsx @@ -4,18 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; import { Case } from '../../../cases/containers/types'; import { getCaseDetailsUrl } from '../../../common/components/link_to/redirect_to_case'; import { Markdown } from '../../../common/components/markdown'; -import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; -import { navTabs } from '../../../app/home/home_navigations'; +import { useFormatUrl } from '../../../common/components/link_to'; import { IconWithCount } from '../recent_timelines/counts'; - +import { LinkAnchor } from '../../../common/components/links'; import * as i18n from './translations'; +import { useKibana } from '../../../common/lib/kibana'; +import { APP_ID } from '../../../../common/constants'; +import { SecurityPageName } from '../../../app/types'; const MarkdownContainer = styled.div` max-height: 150px; @@ -24,7 +26,8 @@ const MarkdownContainer = styled.div` `; const RecentCasesComponent = ({ cases }: { cases: Case[] }) => { - const search = useGetUrlSearch(navTabs.case); + const { formatUrl, search } = useFormatUrl(SecurityPageName.case); + const { navigateToApp } = useKibana().services.application; return ( <> @@ -32,7 +35,17 @@ const RecentCasesComponent = ({ cases }: { cases: Case[] }) => { <EuiFlexGroup key={c.id} gutterSize="none" justifyContent="spaceBetween"> <EuiFlexItem grow={false}> <EuiText size="s"> - <EuiLink href={getCaseDetailsUrl({ id: c.id, search })}>{c.title}</EuiLink> + <LinkAnchor + onClick={(ev: { preventDefault: () => void }) => { + ev.preventDefault(); + navigateToApp(`${APP_ID}:${SecurityPageName.case}`, { + path: getCaseDetailsUrl({ id: c.id, search }), + }); + }} + href={formatUrl(getCaseDetailsUrl({ id: c.id }))} + > + {c.title} + </LinkAnchor> </EuiText> <IconWithCount count={c.totalComment} icon={'editorComment'} tooltip={i18n.COMMENTS} /> diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/index.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/index.tsx index ab76219b3cc003..9c149a850bec9d 100644 --- a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/index.tsx @@ -5,7 +5,7 @@ */ import ApolloClient from 'apollo-client'; -import { EuiHorizontalRule, EuiLink, EuiText } from '@elastic/eui'; +import { EuiHorizontalRule, EuiText } from '@elastic/eui'; import React, { useCallback, useMemo, useEffect } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { Dispatch } from 'redux'; @@ -23,10 +23,12 @@ import { updateIsLoading as dispatchUpdateIsLoading } from '../../../timelines/s import { RecentTimelines } from './recent_timelines'; import * as i18n from './translations'; import { FilterMode } from './types'; -import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; -import { navTabs } from '../../../app/home/home_navigations'; -import { getTimelinesUrl } from '../../../common/components/link_to/redirect_to_timelines'; import { LoadingPlaceholders } from '../loading_placeholders'; +import { useKibana } from '../../../common/lib/kibana'; +import { SecurityPageName } from '../../../app/types'; +import { APP_ID } from '../../../../common/constants'; +import { useFormatUrl } from '../../../common/components/link_to'; +import { LinkAnchor } from '../../../common/components/links'; interface OwnProps { apolloClient: ApolloClient<{}>; @@ -39,6 +41,8 @@ const PAGE_SIZE = 3; const StatefulRecentTimelinesComponent = React.memo<Props>( ({ apolloClient, filterBy, updateIsLoading, updateTimeline }) => { + const { formatUrl } = useFormatUrl(SecurityPageName.timelines); + const { navigateToApp } = useKibana().services.application; const onOpenTimeline: OnOpenTimeline = useCallback( ({ duplicate, timelineId }: { duplicate: boolean; timelineId: string }) => { queryTimelineById({ @@ -52,12 +56,24 @@ const StatefulRecentTimelinesComponent = React.memo<Props>( [apolloClient, updateIsLoading, updateTimeline] ); + const goToTimelines = useCallback( + (ev) => { + ev.preventDefault(); + navigateToApp(`${APP_ID}:${SecurityPageName.timelines}`); + }, + [navigateToApp] + ); + const noTimelinesMessage = filterBy === 'favorites' ? i18n.NO_FAVORITE_TIMELINES : i18n.NO_TIMELINES; - const urlSearch = useGetUrlSearch(navTabs.timelines); + const linkAllTimelines = useMemo( - () => <EuiLink href={getTimelinesUrl(urlSearch)}>{i18n.VIEW_ALL_TIMELINES}</EuiLink>, - [urlSearch] + () => ( + <LinkAnchor onClick={goToTimelines} href={formatUrl('')}> + {i18n.VIEW_ALL_TIMELINES} + </LinkAnchor> + ), + [goToTimelines, formatUrl] ); const loadingPlaceholders = useMemo( () => ( @@ -68,22 +84,24 @@ const StatefulRecentTimelinesComponent = React.memo<Props>( const { fetchAllTimeline, timelines, loading } = useGetAllTimeline(); - useEffect(() => { - fetchAllTimeline({ - pageInfo: { - pageIndex: 1, - pageSize: PAGE_SIZE, - }, - search: '', - sort: { - sortField: SortFieldTimeline.updated, - sortOrder: Direction.desc, - }, - onlyUserFavorite: filterBy === 'favorites', - timelineType: TimelineType.default, - }); + useEffect( + () => + fetchAllTimeline({ + pageInfo: { + pageIndex: 1, + pageSize: PAGE_SIZE, + }, + search: '', + sort: { + sortField: SortFieldTimeline.updated, + sortOrder: Direction.desc, + }, + onlyUserFavorite: filterBy === 'favorites', + timelineType: TimelineType.default, + }), // eslint-disable-next-line react-hooks/exhaustive-deps - }, [filterBy]); + [filterBy] + ); return ( <> diff --git a/x-pack/plugins/security_solution/public/overview/index.ts b/x-pack/plugins/security_solution/public/overview/index.ts index bdf855b3851c82..dad9d0c1dc1f60 100644 --- a/x-pack/plugins/security_solution/public/overview/index.ts +++ b/x-pack/plugins/security_solution/public/overview/index.ts @@ -5,14 +5,14 @@ */ import { SecuritySubPlugin } from '../app/types'; -import { getOverviewRoutes } from './routes'; +import { OverviewRoutes } from './routes'; export class Overview { public setup() {} public start(): SecuritySubPlugin { return { - routes: getOverviewRoutes(), + SubPluginRoutes: OverviewRoutes, }; } } diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx index 57a82f6f254f27..543dafd50c8e03 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx @@ -28,6 +28,7 @@ import { SignalsByCategory } from '../components/signals_by_category'; import { inputsSelectors, State } from '../../common/store'; import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../common/store/inputs/actions'; import { SpyRoute } from '../../common/utils/route/spy_routes'; +import { SecurityPageName } from '../../app/types'; const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; const NO_FILTERS: Filter[] = []; @@ -120,7 +121,7 @@ const OverviewComponent: React.FC<PropsFromRedux> = ({ } </WithSource> - <SpyRoute /> + <SpyRoute pageName={SecurityPageName.overview} /> </> ); diff --git a/x-pack/plugins/security_solution/public/overview/routes.tsx b/x-pack/plugins/security_solution/public/overview/routes.tsx index fc41227b27c04f..ff26475d7b259d 100644 --- a/x-pack/plugins/security_solution/public/overview/routes.tsx +++ b/x-pack/plugins/security_solution/public/overview/routes.tsx @@ -5,11 +5,14 @@ */ import React from 'react'; -import { Route } from 'react-router-dom'; +import { Route, Switch } from 'react-router-dom'; import { Overview } from './pages'; -import { SiemPageName } from '../app/types'; +import { NotFoundPage } from '../app/404'; -export const getOverviewRoutes = () => [ - <Route path={`/:pageName(${SiemPageName.overview})`} render={() => <Overview />} />, -]; +export const OverviewRoutes = () => ( + <Switch> + <Route path="/" render={() => <Overview />} /> + <Route render={() => <NotFoundPage />} /> + </Switch> +); diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index b9eef9f799d3b9..58f0a0ddb749e9 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -5,6 +5,9 @@ */ import { i18n } from '@kbn/i18n'; +import { Store, Action } from 'redux'; +import { BehaviorSubject } from 'rxjs'; +import { pluck } from 'rxjs/operators'; import { AppMountParameters, @@ -20,11 +23,26 @@ import { initTelemetry } from './common/lib/telemetry'; import { KibanaServices } from './common/lib/kibana/services'; import { serviceNowActionType, jiraActionType } from './common/lib/connectors'; import { PluginSetup, PluginStart, SetupPlugins, StartPlugins, StartServices } from './types'; -import { APP_ID, APP_NAME, APP_ICON, APP_PATH } from '../common/constants'; +import { + APP_ID, + APP_ICON, + APP_ALERTS_PATH, + APP_HOSTS_PATH, + APP_OVERVIEW_PATH, + APP_NETWORK_PATH, + APP_TIMELINES_PATH, + APP_MANAGEMENT_PATH, + APP_CASES_PATH, +} from '../common/constants'; import { ConfigureEndpointDatasource } from './management/pages/policy/view/ingest_manager_integration/configure_datasource'; +import { State, createStore, createInitialState } from './common/store'; +import { SecurityPageName } from './app/types'; +import { manageOldSiemRoutes } from './helpers'; + export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, StartPlugins> { private kibanaVersion: string; + private store!: Store<State, Action>; constructor(initializerContext: PluginInitializerContext) { this.kibanaVersion = initializerContext.env.packageInfo.version; @@ -42,7 +60,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S defaultMessage: 'Explore security metrics and logs for events and alerts', }), icon: APP_ICON, - path: APP_PATH, + path: APP_OVERVIEW_PATH, showOnHomePage: true, category: FeatureCatalogueCategory.DATA, }); @@ -50,90 +68,237 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S plugins.triggers_actions_ui.actionTypeRegistry.register(serviceNowActionType()); plugins.triggers_actions_ui.actionTypeRegistry.register(jiraActionType()); - const mountSecurityApp = async (params: AppMountParameters) => { + const mountSecurityFactory = async () => { const storage = new Storage(localStorage); const [coreStart, startPlugins] = await core.getStartServices(); - const { renderApp } = await import('./app'); + if (this.store == null) { + await this.buildStore(coreStart, startPlugins, storage); + } + const services = { ...coreStart, ...startPlugins, - security: plugins.security, storage, + security: plugins.security, } as StartServices; - - const alertsSubPlugin = new (await import('./alerts')).Alerts(); - const casesSubPlugin = new (await import('./cases')).Cases(); - const hostsSubPlugin = new (await import('./hosts')).Hosts(); - const networkSubPlugin = new (await import('./network')).Network(); - const overviewSubPlugin = new (await import('./overview')).Overview(); - const timelinesSubPlugin = new (await import('./timelines')).Timelines(); - const endpointAlertsSubPlugin = new (await import('./endpoint_alerts')).EndpointAlerts(); - const managementSubPlugin = new (await import('./management')).Management(); - - const alertsStart = alertsSubPlugin.start(storage); - const casesStart = casesSubPlugin.start(); - const hostsStart = hostsSubPlugin.start(storage); - const networkStart = networkSubPlugin.start(storage); - const overviewStart = overviewSubPlugin.start(); - const timelinesStart = timelinesSubPlugin.start(); - const endpointAlertsStart = endpointAlertsSubPlugin.start(coreStart, startPlugins); - const managementSubPluginStart = managementSubPlugin.start(coreStart, startPlugins); - - const timelineInitialState = { - timeline: { - ...timelinesStart.store.initialState.timeline!, - timelineById: { - ...timelinesStart.store.initialState.timeline!.timelineById, - ...alertsStart.storageTimelines!.timelineById, - ...hostsStart.storageTimelines!.timelineById, - ...networkStart.storageTimelines!.timelineById, - }, - }, - }; - - return renderApp(services, params, { - routes: [ - ...alertsStart.routes, - ...casesStart.routes, - ...hostsStart.routes, - ...networkStart.routes, - ...overviewStart.routes, - ...timelinesStart.routes, - ...endpointAlertsStart.routes, - ...managementSubPluginStart.routes, - ], - store: { - initialState: { - ...hostsStart.store.initialState, - ...networkStart.store.initialState, - ...timelineInitialState, - ...endpointAlertsStart.store.initialState, - ...managementSubPluginStart.store.initialState, - }, - reducer: { - ...hostsStart.store.reducer, - ...networkStart.store.reducer, - ...timelinesStart.store.reducer, - ...endpointAlertsStart.store.reducer, - ...managementSubPluginStart.store.reducer, - }, - middlewares: [ - ...(endpointAlertsStart.store.middleware ?? []), - ...(managementSubPluginStart.store.middleware ?? []), - ], - }, - }); + return { coreStart, startPlugins, services, store: this.store, storage }; }; + // Waiting for https://github.com/elastic/kibana/issues/69110 + // core.application.register({ + // id: APP_ID, + // title: 'Security', + // appRoute: APP_PATH, + // navLinkStatus: AppNavLinkStatus.hidden, + // mount: async (params: AppMountParameters) => { + // const [{ application }] = await core.getStartServices(); + // application.navigateToApp(`${APP_ID}:${SecurityPageName.overview}`, { replace: true }); + // return () => true; + // }, + // }); + core.application.register({ - id: APP_ID, - title: APP_NAME, + id: `${APP_ID}:${SecurityPageName.overview}`, + title: i18n.translate('xpack.securitySolution.overviewPage.title', { + defaultMessage: 'Overview', + }), order: 9000, euiIconType: APP_ICON, category: DEFAULT_APP_CATEGORIES.security, - appRoute: APP_PATH, - async mount(params: AppMountParameters) { - return mountSecurityApp(params); + appRoute: APP_OVERVIEW_PATH, + mount: async (params: AppMountParameters) => { + const [ + { coreStart, store, services }, + { renderApp, composeLibs }, + { overviewSubPlugin }, + ] = await Promise.all([ + mountSecurityFactory(), + this.downloadAssets(), + this.downloadSubPlugins(), + ]); + return renderApp({ + ...composeLibs(coreStart), + ...params, + services, + store, + SubPluginRoutes: overviewSubPlugin.start().SubPluginRoutes, + }); + }, + }); + + core.application.register({ + id: `${APP_ID}:${SecurityPageName.alerts}`, + title: i18n.translate('xpack.securitySolution.alertsPage.title', { + defaultMessage: 'Alerts', + }), + order: 9001, + euiIconType: APP_ICON, + category: DEFAULT_APP_CATEGORIES.security, + appRoute: APP_ALERTS_PATH, + mount: async (params: AppMountParameters) => { + const [ + { coreStart, store, services, storage }, + { renderApp, composeLibs }, + { alertsSubPlugin }, + ] = await Promise.all([ + mountSecurityFactory(), + this.downloadAssets(), + this.downloadSubPlugins(), + ]); + return renderApp({ + ...composeLibs(coreStart), + ...params, + services, + store, + SubPluginRoutes: alertsSubPlugin.start(storage).SubPluginRoutes, + }); + }, + }); + + core.application.register({ + id: `${APP_ID}:${SecurityPageName.hosts}`, + title: 'Hosts', + order: 9002, + euiIconType: APP_ICON, + category: DEFAULT_APP_CATEGORIES.security, + appRoute: APP_HOSTS_PATH, + mount: async (params: AppMountParameters) => { + const [ + { coreStart, store, services, storage }, + { renderApp, composeLibs }, + { hostsSubPlugin }, + ] = await Promise.all([ + mountSecurityFactory(), + this.downloadAssets(), + this.downloadSubPlugins(), + ]); + return renderApp({ + ...composeLibs(coreStart), + ...params, + services, + store, + SubPluginRoutes: hostsSubPlugin.start(storage).SubPluginRoutes, + }); + }, + }); + + core.application.register({ + id: `${APP_ID}:${SecurityPageName.network}`, + title: 'Network', + order: 9002, + euiIconType: APP_ICON, + category: DEFAULT_APP_CATEGORIES.security, + appRoute: APP_NETWORK_PATH, + mount: async (params: AppMountParameters) => { + const [ + { coreStart, store, services, storage }, + { renderApp, composeLibs }, + { networkSubPlugin }, + ] = await Promise.all([ + mountSecurityFactory(), + this.downloadAssets(), + this.downloadSubPlugins(), + ]); + return renderApp({ + ...composeLibs(coreStart), + ...params, + services, + store, + SubPluginRoutes: networkSubPlugin.start(storage).SubPluginRoutes, + }); + }, + }); + + core.application.register({ + id: `${APP_ID}:${SecurityPageName.timelines}`, + title: 'Timelines', + order: 9002, + euiIconType: APP_ICON, + category: DEFAULT_APP_CATEGORIES.security, + appRoute: APP_TIMELINES_PATH, + mount: async (params: AppMountParameters) => { + const [ + { coreStart, store, services }, + { renderApp, composeLibs }, + { timelinesSubPlugin }, + ] = await Promise.all([ + mountSecurityFactory(), + this.downloadAssets(), + this.downloadSubPlugins(), + ]); + return renderApp({ + ...composeLibs(coreStart), + ...params, + services, + store, + SubPluginRoutes: timelinesSubPlugin.start().SubPluginRoutes, + }); + }, + }); + + core.application.register({ + id: `${APP_ID}:${SecurityPageName.case}`, + title: 'Cases', + order: 9002, + euiIconType: APP_ICON, + category: DEFAULT_APP_CATEGORIES.security, + appRoute: APP_CASES_PATH, + mount: async (params: AppMountParameters) => { + const [ + { coreStart, store, services }, + { renderApp, composeLibs }, + { casesSubPlugin }, + ] = await Promise.all([ + mountSecurityFactory(), + this.downloadAssets(), + this.downloadSubPlugins(), + ]); + return renderApp({ + ...composeLibs(coreStart), + ...params, + services, + store, + SubPluginRoutes: casesSubPlugin.start().SubPluginRoutes, + }); + }, + }); + + core.application.register({ + id: `${APP_ID}:${SecurityPageName.management}`, + title: 'Management', + order: 9002, + euiIconType: APP_ICON, + category: DEFAULT_APP_CATEGORIES.security, + appRoute: APP_MANAGEMENT_PATH, + mount: async (params: AppMountParameters) => { + const [ + { coreStart, startPlugins, store, services }, + { renderApp, composeLibs }, + { managementSubPlugin }, + ] = await Promise.all([ + mountSecurityFactory(), + this.downloadAssets(), + this.downloadSubPlugins(), + ]); + return renderApp({ + ...composeLibs(coreStart), + ...params, + services, + store, + SubPluginRoutes: managementSubPlugin.start(coreStart, startPlugins).SubPluginRoutes, + }); + }, + }); + + core.application.register({ + id: 'siem', + appRoute: 'app/siem', + title: 'SIEM', + navLinkStatus: 3, + mount: async (params: AppMountParameters) => { + const [coreStart] = await core.getStartServices(); + manageOldSiemRoutes(coreStart); + return () => true; }, }); @@ -150,4 +315,97 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S public stop() { return {}; } + + private async downloadAssets() { + const [{ renderApp }, { composeLibs }] = await Promise.all([ + import('./app'), + import('./common/lib/compose/kibana_compose'), + ]); + + return { + renderApp, + composeLibs, + }; + } + + private async downloadSubPlugins() { + const { + alertsSubPlugin, + casesSubPlugin, + hostsSubPlugin, + networkSubPlugin, + overviewSubPlugin, + timelinesSubPlugin, + endpointAlertsSubPlugin, + managementSubPlugin, + } = await import('./sub_plugins'); + + return { + alertsSubPlugin, + casesSubPlugin, + hostsSubPlugin, + networkSubPlugin, + overviewSubPlugin, + timelinesSubPlugin, + endpointAlertsSubPlugin, + managementSubPlugin, + }; + } + + private async buildStore(coreStart: CoreStart, startPlugins: StartPlugins, storage: Storage) { + const { composeLibs } = await this.downloadAssets(); + + const { + alertsSubPlugin, + hostsSubPlugin, + networkSubPlugin, + timelinesSubPlugin, + endpointAlertsSubPlugin, + managementSubPlugin, + } = await this.downloadSubPlugins(); + + const libs$ = new BehaviorSubject(composeLibs(coreStart)); + + const alertsStart = alertsSubPlugin.start(storage); + const hostsStart = hostsSubPlugin.start(storage); + const networkStart = networkSubPlugin.start(storage); + const timelinesStart = timelinesSubPlugin.start(); + const endpointAlertsStart = endpointAlertsSubPlugin.start(coreStart, startPlugins); + const managementSubPluginStart = managementSubPlugin.start(coreStart, startPlugins); + + const timelineInitialState = { + timeline: { + ...timelinesStart.store.initialState.timeline!, + timelineById: { + ...timelinesStart.store.initialState.timeline!.timelineById, + ...alertsStart.storageTimelines!.timelineById, + ...hostsStart.storageTimelines!.timelineById, + ...networkStart.storageTimelines!.timelineById, + }, + }, + }; + + this.store = createStore( + createInitialState({ + ...hostsStart.store.initialState, + ...networkStart.store.initialState, + ...timelineInitialState, + ...endpointAlertsStart.store.initialState, + ...managementSubPluginStart.store.initialState, + }), + { + ...hostsStart.store.reducer, + ...networkStart.store.reducer, + ...timelinesStart.store.reducer, + ...endpointAlertsStart.store.reducer, + ...managementSubPluginStart.store.reducer, + }, + libs$.pipe(pluck('apolloClient')), + storage, + [ + ...(endpointAlertsStart.store.middleware ?? []), + ...(managementSubPluginStart.store.middleware ?? []), + ] + ); + } } diff --git a/x-pack/plugins/security_solution/public/sub_plugins.ts b/x-pack/plugins/security_solution/public/sub_plugins.ts new file mode 100644 index 00000000000000..553184727db2b6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/sub_plugins.ts @@ -0,0 +1,34 @@ +/* + * 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 { Alerts } from './alerts'; +import { Cases } from './cases'; +import { Hosts } from './hosts'; +import { Network } from './network'; +import { Overview } from './overview'; +import { Timelines } from './timelines'; +import { EndpointAlerts } from './endpoint_alerts'; +import { Management } from './management'; + +const alertsSubPlugin = new Alerts(); +const casesSubPlugin = new Cases(); +const hostsSubPlugin = new Hosts(); +const networkSubPlugin = new Network(); +const overviewSubPlugin = new Overview(); +const timelinesSubPlugin = new Timelines(); +const endpointAlertsSubPlugin = new EndpointAlerts(); +const managementSubPlugin = new Management(); + +export { + alertsSubPlugin, + casesSubPlugin, + hostsSubPlugin, + networkSubPlugin, + overviewSubPlugin, + timelinesSubPlugin, + endpointAlertsSubPlugin, + managementSubPlugin, +}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx index c556c2d53f7c26..38b3068f3d3aa0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx @@ -44,7 +44,7 @@ interface OwnProps { type Props = OwnProps & ProsFromRedux; export const FlyoutComponent = React.memo<Props>( - ({ dataProviders, flyoutHeight, show, showTimeline, timelineId, usersViewing, width }) => { + ({ dataProviders, flyoutHeight, show = true, showTimeline, timelineId, usersViewing, width }) => { const handleClose = useCallback(() => showTimeline({ id: timelineId, show: false }), [ showTimeline, timelineId, diff --git a/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx index 2615d5057347e0..24f8d910b4febb 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx @@ -120,6 +120,8 @@ const getNetflowInstance = () => ( /> ); +jest.mock('../../../common/components/link_to'); + describe('Netflow', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx index 344f10dfdb35e8..24dee1460810fa 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.tsx @@ -109,19 +109,25 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>( const { timelineType, timelineTabs, timelineFilters } = useTimelineTypes(); const { fetchAllTimeline, timelines, loading, totalCount } = useGetAllTimeline(); - const refetch = useCallback(() => { - fetchAllTimeline({ - pageInfo: { - pageIndex: pageIndex + 1, - pageSize, - }, - search, - sort: { sortField: sortField as SortFieldTimeline, sortOrder: sortDirection as Direction }, - onlyUserFavorite: onlyFavorites, - timelineType, - }); + const refetch = useCallback( + () => + fetchAllTimeline({ + pageInfo: { + pageIndex: pageIndex + 1, + pageSize, + }, + search, + sort: { + sortField: sortField as SortFieldTimeline, + sortOrder: sortDirection as Direction, + }, + onlyUserFavorite: onlyFavorites, + timelineType, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pageIndex, pageSize, search, sortField, sortDirection, timelineType, onlyFavorites]); + [pageIndex, pageSize, search, sortField, sortDirection, timelineType, onlyFavorites] + ); /** Invoked when the user presses enters to submit the text in the search input */ const onQueryChange: OnQueryChange = useCallback((query: EuiSearchBarQuery) => { @@ -251,9 +257,7 @@ export const StatefulOpenTimelineComponent = React.memo<OpenTimelineOwnProps>( focusInput(); }, []); - useEffect(() => { - refetch(); - }, [refetch]); + useEffect(() => refetch(), [refetch]); return !isModal ? ( <OpenTimeline diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts index 105f1adf05a628..e1515a3a79254d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts @@ -200,4 +200,5 @@ export interface TimelineTab { name: string; disabled: boolean; href: string; + onClick: (ev: { preventDefault: () => void }) => void; } diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx index afc66e337d7b23..56c67b0c294a22 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx @@ -4,14 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { useState, useCallback, useMemo } from 'react'; -import { useParams } from 'react-router-dom'; +import { useParams, useHistory } from 'react-router-dom'; import { EuiTabs, EuiTab, EuiSpacer, EuiFilterButton } from '@elastic/eui'; import { TimelineTypeLiteralWithNull, TimelineType } from '../../../../common/types/timeline'; - -import { getTimelineTabsUrl } from '../../../common/components/link_to'; -import { navTabs } from '../../../app/home/home_navigations'; -import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; +import { SecurityPageName } from '../../../app/types'; +import { getTimelineTabsUrl, useFormatUrl } from '../../../common/components/link_to'; import * as i18n from './translations'; import { TimelineTabsStyle, TimelineTab } from './types'; @@ -20,12 +18,29 @@ export const useTimelineTypes = (): { timelineTabs: JSX.Element; timelineFilters: JSX.Element; } => { - const urlSearch = useGetUrlSearch(navTabs.timelines); + const history = useHistory(); + const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.timelines); const { tabName } = useParams<{ pageName: string; tabName: string }>(); const [timelineType, setTimelineTypes] = useState<TimelineTypeLiteralWithNull>( tabName === TimelineType.default || tabName === TimelineType.template ? tabName : null ); + const goToTimeline = useCallback( + (ev) => { + ev.preventDefault(); + history.push(getTimelineTabsUrl(TimelineType.default, urlSearch)); + }, + [history, urlSearch] + ); + + const goToTemplateTimeline = useCallback( + (ev) => { + ev.preventDefault(); + history.push(getTimelineTabsUrl(TimelineType.template, urlSearch)); + }, + [history, urlSearch] + ); + const getFilterOrTabs: (timelineTabsStyle: TimelineTabsStyle) => TimelineTab[] = ( timelineTabsStyle: TimelineTabsStyle ) => [ @@ -35,8 +50,9 @@ export const useTimelineTypes = (): { timelineTabsStyle === TimelineTabsStyle.filter ? i18n.FILTER_TIMELINES(i18n.TAB_TIMELINES) : i18n.TAB_TIMELINES, - href: getTimelineTabsUrl(TimelineType.default, urlSearch), + href: formatUrl(getTimelineTabsUrl(TimelineType.default, urlSearch)), disabled: false, + onClick: goToTimeline, }, { id: TimelineType.template, @@ -44,8 +60,9 @@ export const useTimelineTypes = (): { timelineTabsStyle === TimelineTabsStyle.filter ? i18n.FILTER_TIMELINES(i18n.TAB_TEMPLATES) : i18n.TAB_TEMPLATES, - href: getTimelineTabsUrl(TimelineType.template, urlSearch), + href: formatUrl(getTimelineTabsUrl(TimelineType.template, urlSearch)), disabled: false, + onClick: goToTemplateTimeline, }, ]; @@ -70,7 +87,10 @@ export const useTimelineTypes = (): { disabled={tab.disabled} key={`timeline-${TimelineTabsStyle.tab}-${tab.id}`} href={tab.href} - onClick={onFilterClicked.bind(null, TimelineTabsStyle.tab, tab.id)} + onClick={(ev) => { + tab.onClick(ev); + onFilterClicked(TimelineTabsStyle.tab, tab.id); + }} > {tab.name} </EuiTab> @@ -89,7 +109,10 @@ export const useTimelineTypes = (): { <EuiFilterButton hasActiveFilters={tab.id === timelineType} key={`timeline-${TimelineTabsStyle.filter}-${tab.id}`} - onClick={onFilterClicked.bind(null, TimelineTabsStyle.filter, tab.id)} + onClick={(ev: { preventDefault: () => void }) => { + tab.onClick(ev); + onFilterClicked.bind(null, TimelineTabsStyle.filter, tab.id); + }} > {tab.name} </EuiFilterButton> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx index 44e866c157531f..775c26e82d27bc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx @@ -24,6 +24,8 @@ const mockSort: Sort = { sortDirection: Direction.desc, }; +jest.mock('../../../../common/components/link_to'); + jest.mock( 'react-visibility-sensor', () => ({ children }: { children: (args: { isVisible: boolean }) => React.ReactNode }) => diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx index ae5e7e2ef789b7..aec463f531448b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx @@ -19,6 +19,7 @@ import { createGenericFileRowRenderer, } from './generic_row_renderer'; +jest.mock('../../../../../../common/components/link_to'); jest.mock('../../../../../../overview/components/events_by_dataset'); describe('GenericRowRenderer', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.test.tsx index 64f4656e7e790a..3e055682d27a46 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.test.tsx @@ -16,6 +16,7 @@ import { FormattedFieldValue } from './formatted_field'; import { HOST_NAME_FIELD_NAME } from './constants'; jest.mock('../../../../../common/lib/kibana'); +jest.mock('../../../../../common/components/link_to'); describe('Events', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx index aaac3e7f7f4d5d..b2588e19800a67 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx @@ -30,7 +30,7 @@ import { RULE_REFERENCE_FIELD_NAME, SIGNAL_RULE_NAME_FIELD_NAME, } from './constants'; -import { renderRuleName, renderEventModule, renderRulReference } from './formatted_field_helpers'; +import { RenderRuleName, renderEventModule, renderRulReference } from './formatted_field_helpers'; // simple black-list to prevent dragging and dropping fields such as message name const columnNamesNotDraggable = [MESSAGE_FIELD_NAME]; @@ -95,7 +95,16 @@ const FormattedFieldValueComponent: React.FC<{ <Bytes contextId={contextId} eventId={eventId} fieldName={fieldName} value={`${value}`} /> ); } else if (fieldName === SIGNAL_RULE_NAME_FIELD_NAME) { - return renderRuleName({ contextId, eventId, fieldName, linkValue, truncate, value }); + return ( + <RenderRuleName + contextId={contextId} + eventId={eventId} + fieldName={fieldName} + linkValue={linkValue} + truncate={truncate} + value={value} + /> + ); } else if (fieldName === EVENT_MODULE_FIELD_NAME) { return renderEventModule({ contextId, eventId, fieldName, linkValue, truncate, value }); } else if (fieldName === RULE_REFERENCE_FIELD_NAME) { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx index b24c262c8a2e2a..81820e2253fc9e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx @@ -6,7 +6,7 @@ import { EuiLink, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiToolTip } from '@elastic/eui'; import { isString, isEmpty } from 'lodash/fp'; -import React from 'react'; +import React, { useCallback } from 'react'; import styled from 'styled-components'; import { DefaultDraggable } from '../../../../../common/components/draggables'; @@ -18,31 +18,49 @@ import { isUrlInvalid } from '../../../../../common/utils/validators'; import endPointSvg from '../../../../../common/utils/logo_endpoint/64_color.svg'; import * as i18n from './translations'; +import { SecurityPageName } from '../../../../../app/types'; +import { useFormatUrl } from '../../../../../common/components/link_to'; +import { useKibana } from '../../../../../common/lib/kibana'; +import { APP_ID } from '../../../../../../common/constants'; +import { LinkAnchor } from '../../../../../common/components/links'; const EventModuleFlexItem = styled(EuiFlexItem)` width: 100%; `; -export const renderRuleName = ({ - contextId, - eventId, - fieldName, - linkValue, - truncate, - value, -}: { +interface RenderRuleNameProps { contextId: string; eventId: string; fieldName: string; linkValue: string | null | undefined; truncate?: boolean; value: string | number | null | undefined; +} + +export const RenderRuleName: React.FC<RenderRuleNameProps> = ({ + contextId, + eventId, + fieldName, + linkValue, + truncate, + value, }) => { const ruleName = `${value}`; const ruleId = linkValue; - + const { search } = useFormatUrl(SecurityPageName.alerts); + const { navigateToApp, getUrlForApp } = useKibana().services.application; const content = truncate ? <TruncatableText>{value}</TruncatableText> : value; + const goToRuleDetails = useCallback( + (ev) => { + ev.preventDefault(); + navigateToApp(`${APP_ID}:${SecurityPageName.alerts}`, { + path: getRuleDetailsUrl(ruleId ?? '', search), + }); + }, + [navigateToApp, ruleId, search] + ); + return isString(value) && ruleName.length > 0 && ruleId != null ? ( <DefaultDraggable field={fieldName} @@ -50,7 +68,14 @@ export const renderRuleName = ({ tooltipContent={value} value={value} > - <EuiLink href={getRuleDetailsUrl(ruleId)}>{content}</EuiLink> + <LinkAnchor + onClick={goToRuleDetails} + href={getUrlForApp(`${APP_ID}:${SecurityPageName.alerts}`, { + path: getRuleDetailsUrl(ruleId, search), + })} + > + {content} + </LinkAnchor> </DefaultDraggable> ) : ( getEmptyTagValue() diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx index 3222f8a2362db8..0b3ea0ce6e4303 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx @@ -17,6 +17,8 @@ import { useMountAppended } from '../../../../../common/utils/use_mount_appended import { rowRenderers } from '.'; import { getRowRenderer } from './get_row_renderer'; +jest.mock('../../../../../common/components/link_to'); + describe('get_column_renderer', () => { let nonSuricata: Ecs; let suricata: Ecs; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx index 7e51c717ebe8c5..5140b9abc60ef3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx @@ -24,6 +24,8 @@ export const justIdAndTimestamp: Ecs = { timestamp: '2018-11-12T19:03:25.936Z', }; +jest.mock('../../../../../../common/components/link_to'); + describe('netflowRowRenderer', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.test.tsx index 2448540f27e23d..b7c2cb7032cc24 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.test.tsx @@ -16,6 +16,8 @@ import { useMountAppended } from '../../../../../common/utils/use_mount_appended import { plainColumnRenderer } from './plain_column_renderer'; import { getValues, deleteItemIdx, findItem } from './helpers'; +jest.mock('../../../../../common/components/link_to'); + describe('plain_column_renderer', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx index d5040cb2523704..14f147c61fca35 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx @@ -13,6 +13,8 @@ import { TestProviders } from '../../../../../../common/mock/test_providers'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; import { SuricataDetails } from './suricata_details'; +jest.mock('../../../../../../common/components/link_to'); + describe('SuricataDetails', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx index a10cd9dc97f6d4..d36d24f41224c7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx @@ -15,6 +15,8 @@ import { TestProviders } from '../../../../../../common/mock/test_providers'; import { suricataRowRenderer } from './suricata_row_renderer'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; +jest.mock('../../../../../../common/components/link_to'); + describe('suricata_row_renderer', () => { const mount = useMountAppended(); let nonSuricata: Ecs; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_details.test.tsx index e622c91e8b8700..8efd8e19443311 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_details.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_details.test.tsx @@ -13,6 +13,8 @@ import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; import { SystemGenericDetails, SystemGenericLine } from './generic_details'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; +jest.mock('../../../../../../common/components/link_to'); + describe('SystemGenericDetails', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.test.tsx index 61cc7f32567efe..6c7a74d840d016 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.test.tsx @@ -13,6 +13,18 @@ import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; import { SystemGenericFileDetails, SystemGenericFileLine } from './generic_file_details'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; +jest.mock('react-router-dom', () => { + const original = jest.requireActual('react-router-dom'); + + return { + ...original, + useHistory: () => ({ + createHref: jest.fn(), + push: jest.fn(), + }), + }; +}); + describe('SystemGenericFileDetails', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx index 26cccc82896eae..bb997b96892702 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx @@ -48,6 +48,7 @@ import { } from './generic_row_renderer'; import * as i18n from './translations'; +jest.mock('../../../../../../common/components/link_to'); jest.mock('../../../../../../overview/components/events_by_dataset'); describe('GenericRowRenderer', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx index d82a0fcb6e644e..04b0e6e5fcfae4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx @@ -11,6 +11,8 @@ import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; import { ZeekDetails } from './zeek_details'; +jest.mock('../../../../../../common/components/link_to'); + describe('ZeekDetails', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx index 2197ccb0ce2e00..2eed6aaf20335f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx @@ -14,6 +14,8 @@ import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; import { zeekRowRenderer } from './zeek_row_renderer'; +jest.mock('../../../../../../common/components/link_to'); + describe('zeek_row_renderer', () => { const mount = useMountAppended(); let nonZeek: Ecs; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx index d9d98f3e6b954e..5ccc8911d1974b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx @@ -74,6 +74,7 @@ describe('StatefulTimeline', () => { filters: [], id: 'foo', isLive: false, + isTimelineExists: false, itemsPerPage: 5, itemsPerPageOptions: [5, 10, 20], kqlMode: 'search', diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx index b53a34041a6893..51cfe8ae33b05e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx @@ -43,6 +43,7 @@ const StatefulTimelineComponent = React.memo<Props>( filters, id, isLive, + isTimelineExists, itemsPerPage, itemsPerPageOptions, kqlMode, @@ -151,7 +152,7 @@ const StatefulTimelineComponent = React.memo<Props>( ); useEffect(() => { - if (createTimeline != null) { + if (createTimeline != null && !isTimelineExists) { createTimeline({ id, columns: defaultHeaders, show: false }); } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -248,6 +249,7 @@ const makeMapStateToProps = () => { filters: timelineFilter, id, isLive: input.policy.kind === 'interval', + isTimelineExists: getTimeline(state, id) != null, itemsPerPage, itemsPerPageOptions, kqlMode, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/use_insert_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/use_insert_timeline.tsx index 6269bc1b4a1a33..c3def9c4cbb292 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/use_insert_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/use_insert_timeline.tsx @@ -17,7 +17,7 @@ export const useInsertTimeline = <T extends FormData>(form: FormHook<T>, fieldNa }); const handleOnTimelineChange = useCallback( (title: string, id: string | null) => { - const builtLink = `${basePath}/app/security#/timelines?timeline=(id:'${id}',isOpen:!t)`; + const builtLink = `${basePath}/app/security/timelines?timeline=(id:'${id}',isOpen:!t)`; const currentValue = form.getFormData()[fieldName]; const newValue: string = [ currentValue.slice(0, cursorPosition.start), diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx index 4d5c16dc30b8e5..f2e7d26c9e8516 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx @@ -23,14 +23,17 @@ import styled from 'styled-components'; import { useHistory } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; +import { APP_ID } from '../../../../../common/constants'; import { TimelineTypeLiteral, TimelineStatus, TimelineType, } from '../../../../../common/types/timeline'; - -import { SiemPageName } from '../../../../app/types'; +import { navTabs } from '../../../../app/home/home_navigations'; +import { SecurityPageName } from '../../../../app/types'; import { timelineSelectors } from '../../../../timelines/store/timeline'; +import { useGetUrlSearch } from '../../../../common/components/navigation/use_get_url_search'; +import { getCreateCaseUrl } from '../../../../common/components/link_to'; import { State } from '../../../../common/store'; import { useKibana } from '../../../../common/lib/kibana'; import { Note } from '../../../../common/lib/note'; @@ -145,16 +148,15 @@ interface NewCaseProps { export const NewCase = React.memo<NewCaseProps>( ({ onClosePopover, timelineId, timelineStatus, timelineTitle }) => { const history = useHistory(); + const urlSearch = useGetUrlSearch(navTabs.case); const dispatch = useDispatch(); const { savedObjectId } = useSelector((state: State) => timelineSelectors.selectTimeline(state, timelineId) ); + const { navigateToApp } = useKibana().services.application; const handleClick = useCallback(() => { onClosePopover(); - history.push({ - pathname: `/${SiemPageName.case}/create`, - }); dispatch( setInsertTimeline({ timelineId, @@ -162,8 +164,15 @@ export const NewCase = React.memo<NewCaseProps>( timelineTitle: timelineTitle.length > 0 ? timelineTitle : i18n.UNTITLED_TIMELINE, }) ); + navigateToApp(`${APP_ID}:${SecurityPageName.case}`, { + path: getCreateCaseUrl(urlSearch), + }); + history.push({ + pathname: `/${SecurityPageName.case}/create`, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps - }, [dispatch, onClosePopover, history, timelineId, timelineTitle]); + }, [dispatch, navigateToApp, onClosePopover, history, timelineId, timelineTitle, urlSearch]); return ( <EuiButtonEmpty diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx index 3f21c29c28139b..3078700a29d76b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx @@ -17,13 +17,13 @@ import { import { createStore, State } from '../../../../common/store'; import { useThrottledResizeObserver } from '../../../../common/components/utils'; import { Properties, showDescriptionThreshold, showNotesThreshold } from '.'; -import { SiemPageName } from '../../../../app/types'; +import { SecurityPageName } from '../../../../app/types'; import { setInsertTimeline } from '../../../store/timeline/actions'; export { nextTick } from '../../../../../../../test_utils'; import { act } from 'react-dom/test-utils'; -jest.mock('../../../../common/components/navigation/use_get_url_search'); +jest.mock('../../../../common/components/link_to'); jest.mock('../../../../common/lib/kibana', () => { const original = jest.requireActual('../../../../common/lib/kibana'); @@ -38,6 +38,7 @@ jest.mock('../../../../common/lib/kibana', () => { crud: true, }, }, + navigateToApp: jest.fn(), }, }, }), @@ -342,7 +343,7 @@ describe('Properties', () => { wrapper.find('[data-test-subj="settings-gear"]').at(0).simulate('click'); wrapper.find('[data-test-subj="attach-timeline-case"]').first().simulate('click'); - expect(mockHistoryPush).toBeCalledWith({ pathname: `/${SiemPageName.case}/create` }); + expect(mockHistoryPush).toBeCalledWith({ pathname: `/${SecurityPageName.case}/create` }); expect(mockDispatch).toBeCalledWith( setInsertTimeline({ timelineId: defaultProps.timelineId, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx index be79c0773bf88e..602a7c8191c7a2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.tsx @@ -7,7 +7,6 @@ import React, { useState, useCallback, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { useHistory } from 'react-router-dom'; import { TimelineStatus, TimelineTypeLiteral } from '../../../../../common/types/timeline'; import { useThrottledResizeObserver } from '../../../../common/components/utils'; import { Note } from '../../../../common/lib/note'; @@ -19,11 +18,14 @@ import { TimelineProperties } from './styles'; import { PropertiesRight } from './properties_right'; import { PropertiesLeft } from './properties_left'; import { AllCasesModal } from '../../../../cases/components/all_cases_modal'; -import { SiemPageName } from '../../../../app/types'; +import { SecurityPageName } from '../../../../app/types'; import * as i18n from './translations'; import { State } from '../../../../common/store'; import { timelineSelectors } from '../../../store/timeline'; import { setInsertTimeline } from '../../../store/timeline/actions'; +import { useKibana } from '../../../../common/lib/kibana'; +import { APP_ID } from '../../../../../common/constants'; +import { getCaseDetailsUrl } from '../../../../common/components/link_to'; type CreateTimeline = ({ id, @@ -91,6 +93,7 @@ export const Properties = React.memo<Props>( updateTitle, usersViewing, }) => { + const { navigateToApp } = useKibana().services.application; const { ref, width = 0 } = useThrottledResizeObserver(300); const [showActions, setShowActions] = useState(false); const [showNotes, setShowNotes] = useState(false); @@ -110,7 +113,6 @@ export const Properties = React.memo<Props>( const [showCaseModal, setShowCaseModal] = useState(false); const onCloseCaseModal = useCallback(() => setShowCaseModal(false), []); const onOpenCaseModal = useCallback(() => setShowCaseModal(true), []); - const history = useHistory(); const currentTimeline = useSelector((state: State) => timelineSelectors.selectTimeline(state, timelineId) ); @@ -118,8 +120,8 @@ export const Properties = React.memo<Props>( const onRowClick = useCallback( (id: string) => { onCloseCaseModal(); - history.push({ - pathname: `/${SiemPageName.case}/${id}`, + navigateToApp(`${APP_ID}:${SecurityPageName.case}`, { + path: getCaseDetailsUrl({ id }), }); dispatch( setInsertTimeline({ @@ -129,7 +131,7 @@ export const Properties = React.memo<Props>( }) ); }, - [onCloseCaseModal, currentTimeline, dispatch, history, timelineId, title] + [navigateToApp, onCloseCaseModal, currentTimeline, dispatch, timelineId, title] ); const datePickerWidth = useMemo( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx index 2e12ebad2f99db..76c3a647a94394 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/selectable_timeline/index.tsx @@ -244,22 +244,24 @@ const SelectableTimelineComponent: React.FC<SelectableTimelineProps> = ({ }, }; - useEffect(() => { - fetchAllTimeline({ - pageInfo: { - pageIndex: 1, - pageSize, - }, - search: searchTimelineValue, - sort: { - sortField: SortFieldTimeline.updated, - sortOrder: Direction.desc, - }, - onlyUserFavorite: onlyFavorites, - timelineType, - }); + useEffect( + () => + fetchAllTimeline({ + pageInfo: { + pageIndex: 1, + pageSize, + }, + search: searchTimelineValue, + sort: { + sortField: SortFieldTimeline.updated, + sortOrder: Direction.desc, + }, + onlyUserFavorite: onlyFavorites, + timelineType, + }), // eslint-disable-next-line react-hooks/exhaustive-deps - }, [onlyFavorites, pageSize, searchTimelineValue, timelineType]); + [onlyFavorites, pageSize, searchTimelineValue, timelineType] + ); return ( <EuiSelectableContainer isLoading={loading}> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx index 2447ebf47463fc..96703941f616e3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx @@ -32,19 +32,15 @@ jest.mock('use-resize-observer/polyfilled'); mockUseResizeObserver.mockImplementation(() => ({})); -jest.mock('react-router-dom', () => { - const originalModule = jest.requireActual('react-router-dom'); - return { - ...originalModule, - useHistory: jest.fn(), - }; -}); jest.mock('../../../common/lib/kibana', () => { const originalModule = jest.requireActual('../../../common/lib/kibana'); return { ...originalModule, useKibana: jest.fn().mockReturnValue({ services: { + application: { + navigateToApp: jest.fn(), + }, uiSettings: { get: jest.fn(), }, diff --git a/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx index 19112221cbfd05..f025cf15181c3e 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/all/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getOr, noop } from 'lodash/fp'; +import { getOr } from 'lodash/fp'; import memoizeOne from 'memoize-one'; import { useCallback, useState, useEffect } from 'react'; import { useDispatch } from 'react-redux'; @@ -86,15 +86,14 @@ export const useGetAllTimeline = (): AllTimelinesArgs => { const dispatch = useDispatch(); const apolloClient = useApolloClient(); const [, dispatchToaster] = useStateToaster(); - const [allTimelines, setAllTimelines] = useState<AllTimelinesArgs>({ - fetchAllTimeline: noop, + const [allTimelines, setAllTimelines] = useState<Omit<AllTimelinesArgs, 'fetchAllTimeline'>>({ loading: false, totalCount: 0, timelines: [], }); const fetchAllTimeline = useCallback( - async ({ onlyUserFavorite, pageInfo, search, sort, timelineType }: AllTimelinesVariables) => { + ({ onlyUserFavorite, pageInfo, search, sort, timelineType }: AllTimelinesVariables) => { let didCancel = false; const abortCtrl = new AbortController(); @@ -139,7 +138,6 @@ export const useGetAllTimeline = (): AllTimelinesArgs => { }) ); setAllTimelines({ - fetchAllTimeline, loading: false, totalCount, timelines: getAllTimeline(JSON.stringify(variables), timelines as TimelineResult[]), @@ -154,7 +152,6 @@ export const useGetAllTimeline = (): AllTimelinesArgs => { dispatchToaster, }); setAllTimelines({ - fetchAllTimeline, loading: false, totalCount: 0, timelines: [], @@ -168,8 +165,7 @@ export const useGetAllTimeline = (): AllTimelinesArgs => { abortCtrl.abort(); }; }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [apolloClient, allTimelines] + [apolloClient, allTimelines, dispatch, dispatchToaster] ); useEffect(() => { diff --git a/x-pack/plugins/security_solution/public/timelines/index.ts b/x-pack/plugins/security_solution/public/timelines/index.ts index 5cce258b10d16e..dde98e2b9edff5 100644 --- a/x-pack/plugins/security_solution/public/timelines/index.ts +++ b/x-pack/plugins/security_solution/public/timelines/index.ts @@ -5,7 +5,7 @@ */ import { SecuritySubPluginWithStore } from '../app/types'; -import { getTimelinesRoutes } from './routes'; +import { TimelinesRoutes } from './routes'; import { initialTimelineState, timelineReducer } from './store/timeline/reducer'; import { TimelineState } from './store/timeline/types'; @@ -14,7 +14,7 @@ export class Timelines { public start(): SecuritySubPluginWithStore<'timeline', TimelineState> { return { - routes: getTimelinesRoutes(), + SubPluginRoutes: TimelinesRoutes, store: { initialState: { timeline: initialTimelineState }, reducer: { timeline: timelineReducer }, diff --git a/x-pack/plugins/security_solution/public/timelines/pages/index.tsx b/x-pack/plugins/security_solution/public/timelines/pages/index.tsx index 73494fb585b307..91f1980309cd02 100644 --- a/x-pack/plugins/security_solution/public/timelines/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/pages/index.tsx @@ -4,24 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ +import { isEmpty } from 'lodash/fp'; import React from 'react'; -import { ApolloConsumer } from 'react-apollo'; -import { Switch, Route, Redirect } from 'react-router-dom'; +import { Switch, Route, useHistory } from 'react-router-dom'; import { ChromeBreadcrumb } from '../../../../../../src/core/public'; import { TimelineType } from '../../../common/types/timeline'; import { TAB_TIMELINES, TAB_TEMPLATES } from '../components/open_timeline/translations'; -import { getTimelinesUrl } from '../../common/components/link_to'; import { TimelineRouteSpyState } from '../../common/utils/route/types'; -import { SiemPageName } from '../../app/types'; - import { TimelinesPage } from './timelines_page'; import { PAGE_TITLE } from './translations'; import { appendSearch } from '../../common/components/link_to/helpers'; -const timelinesPagePath = `/:pageName(${SiemPageName.timelines})/:tabName(${TimelineType.default}|${TimelineType.template})`; -const timelinesDefaultPath = `/${SiemPageName.timelines}/${TimelineType.default}`; +import { GetUrlForApp } from '../../common/components/navigation/types'; +import { APP_ID } from '../../../common/constants'; +import { SecurityPageName } from '../../app/types'; + +const timelinesPagePath = `/:tabName(${TimelineType.default}|${TimelineType.template})`; +const timelinesDefaultPath = `/${TimelineType.default}`; const TabNameMappedToI18nKey: Record<string, string> = { [TimelineType.default]: TAB_TIMELINES, @@ -30,12 +31,15 @@ const TabNameMappedToI18nKey: Record<string, string> = { export const getBreadcrumbs = ( params: TimelineRouteSpyState, - search: string[] + search: string[], + getUrlForApp: GetUrlForApp ): ChromeBreadcrumb[] => { let breadcrumb = [ { text: PAGE_TITLE, - href: `${getTimelinesUrl(appendSearch(search[1]))}`, + href: getUrlForApp(`${APP_ID}:${SecurityPageName.timelines}`, { + path: !isEmpty(search[0]) ? search[0] : '', + }), }, ]; @@ -53,16 +57,18 @@ export const getBreadcrumbs = ( }; export const Timelines = React.memo(() => { + const history = useHistory(); return ( <Switch> <Route exact path={timelinesPagePath}> - <ApolloConsumer>{(client) => <TimelinesPage apolloClient={client} />}</ApolloConsumer> + <TimelinesPage /> </Route> <Route - path={`/${SiemPageName.timelines}/`} - render={({ location: { search = '' } }) => ( - <Redirect to={`${timelinesDefaultPath}${appendSearch(search)}`} /> - )} + path="/" + render={({ location: { search = '' } }) => { + history.replace(`${timelinesDefaultPath}${appendSearch(search)}`); + return null; + }} /> </Switch> ); diff --git a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.test.tsx b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.test.tsx index 3a5e3b56e5cc47..1bd5874394df3b 100644 --- a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.test.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import ApolloClient from 'apollo-client'; import { shallow, ShallowWrapper } from 'enzyme'; import React from 'react'; @@ -21,7 +20,6 @@ jest.mock('../../common/lib/kibana', () => { }); describe('TimelinesPageComponent', () => { - const mockAppollloClient = {} as ApolloClient<object>; let wrapper: ShallowWrapper; describe('If the user is authorized', () => { @@ -37,7 +35,7 @@ describe('TimelinesPageComponent', () => { }, }, }); - wrapper = shallow(<TimelinesPageComponent apolloClient={mockAppollloClient} />); + wrapper = shallow(<TimelinesPageComponent />); }); afterAll(() => { @@ -89,7 +87,7 @@ describe('TimelinesPageComponent', () => { }, }, }); - wrapper = shallow(<TimelinesPageComponent apolloClient={mockAppollloClient} />); + wrapper = shallow(<TimelinesPageComponent />); }); afterAll(() => { diff --git a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx index 95a2e4f5fd0ee9..089a928403b0b1 100644 --- a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx +++ b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx @@ -5,7 +5,6 @@ */ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import ApolloClient from 'apollo-client'; import React, { useCallback, useState } from 'react'; import styled from 'styled-components'; @@ -15,6 +14,7 @@ import { HeaderPage } from '../../common/components/header_page'; import { WrapperPage } from '../../common/components/wrapper_page'; import { useKibana } from '../../common/lib/kibana'; import { SpyRoute } from '../../common/utils/route/spy_routes'; +import { useApolloClient } from '../../common/utils/apollo_context'; import { StatefulOpenTimeline } from '../components/open_timeline'; import { NEW_TEMPLATE_TIMELINE } from '../components/timeline/properties/translations'; @@ -22,25 +22,21 @@ import { NewTemplateTimeline } from '../components/timeline/properties/new_templ import { NewTimeline } from '../components/timeline/properties/helpers'; import * as i18n from './translations'; +import { SecurityPageName } from '../../app/types'; const TimelinesContainer = styled.div` width: 100%; `; -interface TimelinesProps<TCache = object> { - apolloClient: ApolloClient<TCache>; -} - -type OwnProps = TimelinesProps; - export const DEFAULT_SEARCH_RESULTS_PER_PAGE = 10; -export const TimelinesPageComponent: React.FC<OwnProps> = ({ apolloClient }) => { +export const TimelinesPageComponent: React.FC = () => { const [importDataModalToggle, setImportDataModalToggle] = useState<boolean>(false); const onImportTimelineBtnClick = useCallback(() => { setImportDataModalToggle(true); }, [setImportDataModalToggle]); + const apolloClient = useApolloClient(); const uiCapabilities = useKibana().services.application.capabilities; const capabilitiesCanUserCRUD: boolean = !!uiCapabilities.siem.crud; @@ -87,7 +83,7 @@ export const TimelinesPageComponent: React.FC<OwnProps> = ({ apolloClient }) => <TimelinesContainer> <StatefulOpenTimeline - apolloClient={apolloClient} + apolloClient={apolloClient!} defaultPageSize={DEFAULT_SEARCH_RESULTS_PER_PAGE} isModal={false} importDataModalToggle={importDataModalToggle && capabilitiesCanUserCRUD} @@ -98,7 +94,7 @@ export const TimelinesPageComponent: React.FC<OwnProps> = ({ apolloClient }) => </TimelinesContainer> </WrapperPage> - <SpyRoute /> + <SpyRoute pageName={SecurityPageName.timelines} /> </> ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/routes.tsx b/x-pack/plugins/security_solution/public/timelines/routes.tsx index 50b8e1b8a71189..4d1d5f603d217d 100644 --- a/x-pack/plugins/security_solution/public/timelines/routes.tsx +++ b/x-pack/plugins/security_solution/public/timelines/routes.tsx @@ -5,11 +5,14 @@ */ import React from 'react'; -import { Route } from 'react-router-dom'; +import { Route, Switch } from 'react-router-dom'; import { Timelines } from './pages'; -import { SiemPageName } from '../app/types'; +import { NotFoundPage } from '../app/404'; -export const getTimelinesRoutes = () => [ - <Route path={`/:pageName(${SiemPageName.timelines})`} render={() => <Timelines />} />, -]; +export const TimelinesRoutes = () => ( + <Switch> + <Route path="/" render={() => <Timelines />} /> + <Route render={() => <NotFoundPage />} /> + </Switch> +); diff --git a/x-pack/test/security_solution_endpoint/config.ts b/x-pack/test/security_solution_endpoint/config.ts index b6c8e52386cb9d..f02a6bdcd51ed4 100644 --- a/x-pack/test/security_solution_endpoint/config.ts +++ b/x-pack/test/security_solution_endpoint/config.ts @@ -22,8 +22,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { services, apps: { ...xpackFunctionalConfig.get('apps'), - endpoint: { - pathname: '/app/endpoint', + ['securitySolutionManagement']: { + pathname: '/app/security/management', }, }, kbnTestServer: { diff --git a/x-pack/test/security_solution_endpoint/page_objects/endpoint_page.ts b/x-pack/test/security_solution_endpoint/page_objects/endpoint_page.ts index 830c5ec5a42d1d..7339903d74a0bc 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/endpoint_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/endpoint_page.ts @@ -17,9 +17,10 @@ export function EndpointPageProvider({ getService, getPageObjects }: FtrProvider * Navigate to the Endpoints list page */ async navigateToEndpointList(searchParams?: string) { - await pageObjects.common.navigateToApp('securitySolution', { - hash: `/management/endpoints${searchParams ? `?${searchParams}` : ''}`, - }); + await pageObjects.common.navigateToUrlWithBrowserHistory( + 'securitySolutionManagement', + `/endpoints${searchParams ? `?${searchParams}` : ''}` + ); await pageObjects.header.waitUntilLoadingHasFinished(); }, diff --git a/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts b/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts index 3ffd7eb032c22f..d07c4e70f26875 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts @@ -15,7 +15,10 @@ export function EndpointPolicyPageProvider({ getService, getPageObjects }: FtrPr * Navigates to the Endpoint Policy List */ async navigateToPolicyList() { - await pageObjects.common.navigateToApp('securitySolution', { hash: '/management/policy' }); + await pageObjects.common.navigateToUrlWithBrowserHistory( + 'securitySolutionManagement', + '/policy' + ); await pageObjects.header.waitUntilLoadingHasFinished(); }, @@ -51,9 +54,10 @@ export function EndpointPolicyPageProvider({ getService, getPageObjects }: FtrPr * @param policyId */ async navigateToPolicyDetails(policyId: string) { - await pageObjects.common.navigateToApp('securitySolution', { - hash: `/management/policy/${policyId}`, - }); + await pageObjects.common.navigateToUrlWithBrowserHistory( + 'securitySolutionManagement', + `/policy/${policyId}` + ); await pageObjects.header.waitUntilLoadingHasFinished(); },