From b8b145e33bde4f18c4eeb38764b0bc62fe7c7473 Mon Sep 17 00:00:00 2001 From: Pablo Neves Machado Date: Wed, 11 May 2022 11:11:02 +0200 Subject: [PATCH 01/25] Update navigation landing pages to use appLinks config --- .../public/app/translations.ts | 6 +- .../__snapshots__/index.test.tsx.snap | 170 ++++++++++++++++++ .../index.test.tsx | 169 +---------------- .../common/hooks/use_experimental_features.ts | 24 +-- .../common/images/detection_response_page.png | Bin 0 -> 35830 bytes .../public/common/links/links.test.ts | 9 + .../public/common/links/links.ts | 17 +- .../public/common/links/types.ts | 15 +- .../security_solution/public/hosts/links.ts | 6 + .../components/landing_links_icons.test.tsx | 20 ++- .../components/landing_links_icons.tsx | 33 +--- .../components/landing_links_images.test.tsx | 18 +- .../components/landing_links_images.tsx | 34 ++-- .../public/landing_pages/constants.ts | 36 ++++ .../public/landing_pages/pages/dashboards.tsx | 35 ++-- .../landing_pages/pages/manage.test.tsx | 84 +++++---- .../public/landing_pages/pages/manage.tsx | 144 ++------------- .../landing_pages/pages/threat_hunting.tsx | 56 ++---- .../icons/blocklist.tsx | 0 .../icons/endpoint_policies.tsx | 0 .../icons/endpoints.tsx | 0 .../icons/event_filters.tsx | 0 .../icons/exception_lists.tsx | 0 .../icons/host_isolation.tsx | 0 .../icons/siem_rules.tsx | 0 .../icons/trusted_applications.tsx | 0 .../public/management/links.ts | 47 +++++ .../security_solution/public/network/links.ts | 6 + .../public/overview/links.ts | 11 ++ .../security_solution/public/users/links.ts | 5 + 30 files changed, 466 insertions(+), 479 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/__snapshots__/index.test.tsx.snap create mode 100644 x-pack/plugins/security_solution/public/common/images/detection_response_page.png create mode 100644 x-pack/plugins/security_solution/public/landing_pages/constants.ts rename x-pack/plugins/security_solution/public/{landing_pages => management}/icons/blocklist.tsx (100%) rename x-pack/plugins/security_solution/public/{landing_pages => management}/icons/endpoint_policies.tsx (100%) rename x-pack/plugins/security_solution/public/{landing_pages => management}/icons/endpoints.tsx (100%) rename x-pack/plugins/security_solution/public/{landing_pages => management}/icons/event_filters.tsx (100%) rename x-pack/plugins/security_solution/public/{landing_pages => management}/icons/exception_lists.tsx (100%) rename x-pack/plugins/security_solution/public/{landing_pages => management}/icons/host_isolation.tsx (100%) rename x-pack/plugins/security_solution/public/{landing_pages => management}/icons/siem_rules.tsx (100%) rename x-pack/plugins/security_solution/public/{landing_pages => management}/icons/trusted_applications.tsx (100%) diff --git a/x-pack/plugins/security_solution/public/app/translations.ts b/x-pack/plugins/security_solution/public/app/translations.ts index aa7eaa83685db..e9a45c0397316 100644 --- a/x-pack/plugins/security_solution/public/app/translations.ts +++ b/x-pack/plugins/security_solution/public/app/translations.ts @@ -43,7 +43,7 @@ export const USERS = i18n.translate('xpack.securitySolution.navigation.users', { }); export const RULES = i18n.translate('xpack.securitySolution.navigation.rules', { - defaultMessage: 'Rules', + defaultMessage: 'SIEM rules', }); export const EXCEPTIONS = i18n.translate('xpack.securitySolution.navigation.exceptions', { @@ -71,7 +71,7 @@ export const ENDPOINTS = i18n.translate('xpack.securitySolution.search.administr export const POLICIES = i18n.translate( 'xpack.securitySolution.navigation.administration.policies', { - defaultMessage: 'Policies', + defaultMessage: 'Endpoint policies', } ); export const TRUSTED_APPLICATIONS = i18n.translate( @@ -90,7 +90,7 @@ export const EVENT_FILTERS = i18n.translate( export const HOST_ISOLATION_EXCEPTIONS = i18n.translate( 'xpack.securitySolution.search.administration.hostIsolationExceptions', { - defaultMessage: 'Host isolation exceptions', + defaultMessage: 'Host isolation IP exceptions', } ); export const DETECT = i18n.translate('xpack.securitySolution.navigation.detect', { diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000000000..b80e8e290f65b --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/__snapshots__/index.test.tsx.snap @@ -0,0 +1,170 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`useSecuritySolutionNavigation should create navigation config 1`] = ` +Object { + "icon": "logoSecurity", + "items": Array [ + Object { + "id": "main", + "items": Array [ + Object { + "data-href": "securitySolutionUI/get_started?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "data-test-subj": "navigation-get_started", + "disabled": false, + "href": "securitySolutionUI/get_started?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "id": "get_started", + "isSelected": false, + "name": "Getting started", + "onClick": [Function], + }, + Object { + "data-href": "securitySolutionUI/overview?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "data-test-subj": "navigation-overview", + "disabled": false, + "href": "securitySolutionUI/overview?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "id": "overview", + "isSelected": false, + "name": "Overview", + "onClick": [Function], + }, + ], + "name": "", + }, + Object { + "id": "detect", + "items": Array [ + Object { + "data-href": "securitySolutionUI/alerts?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "data-test-subj": "navigation-alerts", + "disabled": false, + "href": "securitySolutionUI/alerts?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "id": "alerts", + "isSelected": false, + "name": "Alerts", + "onClick": [Function], + }, + Object { + "data-href": "securitySolutionUI/rules?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "data-test-subj": "navigation-rules", + "disabled": false, + "href": "securitySolutionUI/rules?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "id": "rules", + "isSelected": false, + "name": "SIEM rules", + "onClick": [Function], + }, + Object { + "data-href": "securitySolutionUI/exceptions?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "data-test-subj": "navigation-exceptions", + "disabled": false, + "href": "securitySolutionUI/exceptions?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "id": "exceptions", + "isSelected": false, + "name": "Exception lists", + "onClick": [Function], + }, + ], + "name": "Detect", + }, + Object { + "id": "explore", + "items": Array [ + Object { + "data-href": "securitySolutionUI/hosts?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "data-test-subj": "navigation-hosts", + "disabled": false, + "href": "securitySolutionUI/hosts?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "id": "hosts", + "isSelected": true, + "name": "Hosts", + "onClick": [Function], + }, + Object { + "data-href": "securitySolutionUI/network?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "data-test-subj": "navigation-network", + "disabled": false, + "href": "securitySolutionUI/network?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "id": "network", + "isSelected": false, + "name": "Network", + "onClick": [Function], + }, + ], + "name": "Explore", + }, + Object { + "id": "investigate", + "items": Array [ + Object { + "data-href": "securitySolutionUI/timelines?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "data-test-subj": "navigation-timelines", + "disabled": false, + "href": "securitySolutionUI/timelines?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "id": "timelines", + "isSelected": false, + "name": "Timelines", + "onClick": [Function], + }, + ], + "name": "Investigate", + }, + Object { + "id": "manage", + "items": Array [ + Object { + "data-href": "securitySolutionUI/endpoints", + "data-test-subj": "navigation-endpoints", + "disabled": false, + "href": "securitySolutionUI/endpoints", + "id": "endpoints", + "isSelected": false, + "name": "Endpoints", + "onClick": [Function], + }, + Object { + "data-href": "securitySolutionUI/trusted_apps", + "data-test-subj": "navigation-trusted_apps", + "disabled": false, + "href": "securitySolutionUI/trusted_apps", + "id": "trusted_apps", + "isSelected": false, + "name": "Trusted applications", + "onClick": [Function], + }, + Object { + "data-href": "securitySolutionUI/event_filters", + "data-test-subj": "navigation-event_filters", + "disabled": false, + "href": "securitySolutionUI/event_filters", + "id": "event_filters", + "isSelected": false, + "name": "Event filters", + "onClick": [Function], + }, + Object { + "data-href": "securitySolutionUI/host_isolation_exceptions", + "data-test-subj": "navigation-host_isolation_exceptions", + "disabled": false, + "href": "securitySolutionUI/host_isolation_exceptions", + "id": "host_isolation_exceptions", + "isSelected": false, + "name": "Host isolation IP exceptions", + "onClick": [Function], + }, + Object { + "data-href": "securitySolutionUI/blocklist", + "data-test-subj": "navigation-blocklist", + "disabled": false, + "href": "securitySolutionUI/blocklist", + "id": "blocklist", + "isSelected": false, + "name": "Blocklist", + "onClick": [Function], + }, + ], + "name": "Manage", + }, + ], + "name": "Security", +} +`; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx index 3f30facd5e41e..dc06162cd906c 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx @@ -108,174 +108,7 @@ describe('useSecuritySolutionNavigation', () => { { wrapper: TestProviders } ); - expect(result.current).toMatchInlineSnapshot(` - Object { - "icon": "logoSecurity", - "items": Array [ - Object { - "id": "main", - "items": Array [ - Object { - "data-href": "securitySolutionUI/get_started?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", - "data-test-subj": "navigation-get_started", - "disabled": false, - "href": "securitySolutionUI/get_started?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", - "id": "get_started", - "isSelected": false, - "name": "Getting started", - "onClick": [Function], - }, - Object { - "data-href": "securitySolutionUI/overview?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", - "data-test-subj": "navigation-overview", - "disabled": false, - "href": "securitySolutionUI/overview?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", - "id": "overview", - "isSelected": false, - "name": "Overview", - "onClick": [Function], - }, - ], - "name": "", - }, - Object { - "id": "detect", - "items": Array [ - Object { - "data-href": "securitySolutionUI/alerts?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", - "data-test-subj": "navigation-alerts", - "disabled": false, - "href": "securitySolutionUI/alerts?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", - "id": "alerts", - "isSelected": false, - "name": "Alerts", - "onClick": [Function], - }, - Object { - "data-href": "securitySolutionUI/rules?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", - "data-test-subj": "navigation-rules", - "disabled": false, - "href": "securitySolutionUI/rules?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", - "id": "rules", - "isSelected": false, - "name": "Rules", - "onClick": [Function], - }, - Object { - "data-href": "securitySolutionUI/exceptions?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", - "data-test-subj": "navigation-exceptions", - "disabled": false, - "href": "securitySolutionUI/exceptions?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", - "id": "exceptions", - "isSelected": false, - "name": "Exception lists", - "onClick": [Function], - }, - ], - "name": "Detect", - }, - Object { - "id": "explore", - "items": Array [ - Object { - "data-href": "securitySolutionUI/hosts?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", - "data-test-subj": "navigation-hosts", - "disabled": false, - "href": "securitySolutionUI/hosts?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", - "id": "hosts", - "isSelected": true, - "name": "Hosts", - "onClick": [Function], - }, - Object { - "data-href": "securitySolutionUI/network?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", - "data-test-subj": "navigation-network", - "disabled": false, - "href": "securitySolutionUI/network?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", - "id": "network", - "isSelected": false, - "name": "Network", - "onClick": [Function], - }, - ], - "name": "Explore", - }, - Object { - "id": "investigate", - "items": Array [ - Object { - "data-href": "securitySolutionUI/timelines?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", - "data-test-subj": "navigation-timelines", - "disabled": false, - "href": "securitySolutionUI/timelines?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", - "id": "timelines", - "isSelected": false, - "name": "Timelines", - "onClick": [Function], - }, - ], - "name": "Investigate", - }, - Object { - "id": "manage", - "items": Array [ - Object { - "data-href": "securitySolutionUI/endpoints", - "data-test-subj": "navigation-endpoints", - "disabled": false, - "href": "securitySolutionUI/endpoints", - "id": "endpoints", - "isSelected": false, - "name": "Endpoints", - "onClick": [Function], - }, - Object { - "data-href": "securitySolutionUI/trusted_apps", - "data-test-subj": "navigation-trusted_apps", - "disabled": false, - "href": "securitySolutionUI/trusted_apps", - "id": "trusted_apps", - "isSelected": false, - "name": "Trusted applications", - "onClick": [Function], - }, - Object { - "data-href": "securitySolutionUI/event_filters", - "data-test-subj": "navigation-event_filters", - "disabled": false, - "href": "securitySolutionUI/event_filters", - "id": "event_filters", - "isSelected": false, - "name": "Event filters", - "onClick": [Function], - }, - Object { - "data-href": "securitySolutionUI/host_isolation_exceptions", - "data-test-subj": "navigation-host_isolation_exceptions", - "disabled": false, - "href": "securitySolutionUI/host_isolation_exceptions", - "id": "host_isolation_exceptions", - "isSelected": false, - "name": "Host isolation exceptions", - "onClick": [Function], - }, - Object { - "data-href": "securitySolutionUI/blocklist", - "data-test-subj": "navigation-blocklist", - "disabled": false, - "href": "securitySolutionUI/blocklist", - "id": "blocklist", - "isSelected": false, - "name": "Blocklist", - "onClick": [Function], - }, - ], - "name": "Manage", - }, - ], - "name": "Security", - } - `); + expect(result.current).toMatchSnapshot(); }); // TODO: Steph/users remove when no longer experimental diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_experimental_features.ts b/x-pack/plugins/security_solution/public/common/hooks/use_experimental_features.ts index 3132ae70381a2..1cc2506ec3996 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_experimental_features.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_experimental_features.ts @@ -14,14 +14,16 @@ import { const allowedExperimentalValues = getExperimentalAllowedValues(); -export const useIsExperimentalFeatureEnabled = (feature: keyof ExperimentalFeatures): boolean => - useSelector(({ app: { enableExperimental } }: State) => { - if (!enableExperimental || !(feature in enableExperimental)) { - throw new Error( - `Invalid enable value ${feature}. Allowed values are: ${allowedExperimentalValues.join( - ', ' - )}` - ); - } - return enableExperimental[feature]; - }); +export const useIsExperimentalFeatureEnabled = (feature: keyof ExperimentalFeatures): boolean => { + const enableExperimental = useEnableExperimental(); + + if (!enableExperimental || !(feature in enableExperimental)) { + throw new Error( + `Invalid enable value ${feature}. Allowed values are: ${allowedExperimentalValues.join(', ')}` + ); + } + return enableExperimental[feature]; +}; + +export const useEnableExperimental = (): ExperimentalFeatures => + useSelector(({ app: { enableExperimental } }: State) => enableExperimental); diff --git a/x-pack/plugins/security_solution/public/common/images/detection_response_page.png b/x-pack/plugins/security_solution/public/common/images/detection_response_page.png new file mode 100644 index 0000000000000000000000000000000000000000..630cd555598432dc0f50c0dd68117e6c35537bf3 GIT binary patch literal 35830 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 { id: 'hosts', path: '/hosts', title: 'Hosts', + landingImage: 'test-file-stub', + description: + 'A computer or other device that communicates with other hosts on a network. Hosts on a network include clients and servers -- that send or receive data, services or applications.', }, ]); }); @@ -383,6 +386,9 @@ describe('security app link helpers', () => { id: 'hosts', path: '/hosts', title: 'Hosts', + landingImage: 'test-file-stub', + description: + 'A computer or other device that communicates with other hosts on a network. Hosts on a network include clients and servers -- that send or receive data, services or applications.', }, { id: 'uncommon_processes', @@ -415,6 +421,9 @@ describe('security app link helpers', () => { id: 'hosts', path: '/hosts', title: 'Hosts', + landingImage: 'test-file-stub', + description: + 'A computer or other device that communicates with other hosts on a network. Hosts on a network include clients and servers -- that send or receive data, services or applications.', }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/links/links.ts b/x-pack/plugins/security_solution/public/common/links/links.ts index 290a1f3fbd820..a150ab2e7e0ce 100644 --- a/x-pack/plugins/security_solution/public/common/links/links.ts +++ b/x-pack/plugins/security_solution/public/common/links/links.ts @@ -8,6 +8,9 @@ import { AppDeepLink, AppNavLinkStatus, Capabilities } from '@kbn/core/public'; import { get } from 'lodash'; import { SecurityPageName } from '../../../common/constants'; +import { useEnableExperimental } from '../hooks/use_experimental_features'; +import { useLicense } from '../hooks/use_license'; +import { useKibana } from '../lib/kibana'; import { appLinks, getAppLinks } from './app_links'; import { Feature, @@ -32,8 +35,6 @@ const createDeepLink = (link: LinkItem, linkProps?: UserPermissions): AppDeepLin }), } : {}), - ...(link.icon != null ? { euiIconType: link.icon } : {}), - ...(link.image != null ? { icon: link.image } : {}), ...(link.globalSearchKeywords != null ? { keywords: link.globalSearchKeywords } : {}), ...(link.globalNavEnabled != null ? { navLinkStatus: link.globalNavEnabled ? AppNavLinkStatus.visible : AppNavLinkStatus.hidden } @@ -47,8 +48,8 @@ const createNavLinkItem = (link: LinkItem, linkProps?: UserPermissions): NavLink path: link.path, title: link.title, ...(link.description != null ? { description: link.description } : {}), - ...(link.icon != null ? { icon: link.icon } : {}), - ...(link.image != null ? { image: link.image } : {}), + ...(link.landingIcon != null ? { icon: link.landingIcon } : {}), + ...(link.landingImage != null ? { image: link.landingImage } : {}), ...(link.links && link.links.length ? { links: reduceLinks({ @@ -195,3 +196,11 @@ export const getAncestorLinksInfo = (id: SecurityPageName): LinkInfo[] => { export const needsUrlState = (id: SecurityPageName): boolean => { return !getNormalizedLink(id).skipUrlState; }; + +export const useAppNavLinks = (): NavLinkItem[] => { + const license = useLicense(); + const enableExperimental = useEnableExperimental(); + const capabilities = useKibana().services.application.capabilities; + + return getNavLinkItems({ enableExperimental, license, capabilities }); +}; diff --git a/x-pack/plugins/security_solution/public/common/links/types.ts b/x-pack/plugins/security_solution/public/common/links/types.ts index eea348b3df737..320c38d1d229b 100644 --- a/x-pack/plugins/security_solution/public/common/links/types.ts +++ b/x-pack/plugins/security_solution/public/common/links/types.ts @@ -7,6 +7,7 @@ import { Capabilities } from '@kbn/core/types'; import { LicenseType } from '@kbn/licensing-plugin/common/types'; +import { IconType } from '@elastic/eui'; import { LicenseService } from '../../../common/license'; import { ExperimentalFeatures } from '../../../common/experimental_features'; import { CASES_FEATURE_ID, SecurityPageName, SERVER_APP_ID } from '../../../common/constants'; @@ -41,9 +42,17 @@ export interface LinkItem { globalSearchEnabled?: boolean; globalSearchKeywords?: string[]; hideWhenExperimentalKey?: keyof ExperimentalFeatures; - icon?: string; id: SecurityPageName; - image?: string; + /** + * Icon that is displayed on menu navigation landing page. + * Only required for pages that are displayed inside a landing page. + */ + landingIcon?: IconType; + /** + * Image that is displayed on menu navigation landing page. + * Only required for pages that are displayed inside a landing page. + */ + landingImage?: string; isBeta?: boolean; licenseType?: LicenseType; links?: LinkItem[]; @@ -54,7 +63,7 @@ export interface LinkItem { export interface NavLinkItem { description?: string; - icon?: string; + icon?: IconType; id: SecurityPageName; links?: NavLinkItem[]; image?: string; diff --git a/x-pack/plugins/security_solution/public/hosts/links.ts b/x-pack/plugins/security_solution/public/hosts/links.ts index 35730291d6c74..421fe9693a57a 100644 --- a/x-pack/plugins/security_solution/public/hosts/links.ts +++ b/x-pack/plugins/security_solution/public/hosts/links.ts @@ -8,10 +8,16 @@ import { i18n } from '@kbn/i18n'; import { HOSTS_PATH, SecurityPageName } from '../../common/constants'; import { HOSTS } from '../app/translations'; import { LinkItem } from '../common/links/types'; +import hostsPageImg from '../common/images/hosts_page.png'; export const links: LinkItem = { id: SecurityPageName.hosts, title: HOSTS, + landingImage: hostsPageImg, + description: i18n.translate('xpack.securitySolution.landing.threatHunting.hostsDescription', { + defaultMessage: + 'A computer or other device that communicates with other hosts on a network. Hosts on a network include clients and servers -- that send or receive data, services or applications.', + }), path: HOSTS_PATH, globalNavEnabled: true, globalSearchKeywords: [ diff --git a/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_icons.test.tsx b/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_icons.test.tsx index 3553f44cc621f..4db27261654ed 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_icons.test.tsx +++ b/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_icons.test.tsx @@ -8,14 +8,16 @@ import { fireEvent, render } from '@testing-library/react'; import React from 'react'; import { SecurityPageName } from '../../app/types'; +import { NavLinkItem } from '../../common/links/types'; import { TestProviders } from '../../common/mock'; -import { LandingLinksIcons, NavItem } from './landing_links_icons'; +import { LandingLinksIcons } from './landing_links_icons'; -const DEFAULT_NAV_ITEM: NavItem = { +const DEFAULT_NAV_ITEM: NavLinkItem = { id: SecurityPageName.overview, - label: 'TEST LABEL', + title: 'TEST LABEL', description: 'TEST DESCRIPTION', icon: 'myTestIcon', + path: '', }; const mockNavigateTo = jest.fn(); @@ -42,28 +44,28 @@ jest.mock('../../common/components/link_to', () => { describe('LandingLinksIcons', () => { it('renders', () => { - const label = 'test label'; + const title = 'test label'; const { queryByText } = render( - + ); - expect(queryByText(label)).toBeInTheDocument(); + expect(queryByText(title)).toBeInTheDocument(); }); it('renders navigation link', () => { const id = SecurityPageName.administration; - const label = 'myTestLable'; + const title = 'myTestLable'; const { getByText } = render( - + ); - fireEvent.click(getByText(label)); + fireEvent.click(getByText(title)); expect(mockNavigateTo).toHaveBeenCalledWith({ url: '/administration' }); }); diff --git a/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_icons.tsx b/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_icons.tsx index 82a0d2148f683..04a3e20b1f178 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_icons.tsx +++ b/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_icons.tsx @@ -4,33 +4,18 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { - EuiFlexGrid, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiText, - EuiTitle, - IconType, -} from '@elastic/eui'; +import { EuiFlexGrid, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, EuiTitle } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; -import { SecurityPageName } from '../../app/types'; + import { SecuritySolutionLinkAnchor, withSecuritySolutionLink, } from '../../common/components/links'; +import { NavLinkItem } from '../../common/links/types'; interface LandingLinksImagesProps { - items: NavItem[]; -} - -export interface NavItem { - id: SecurityPageName; - label: string; - icon: IconType; - description: string; - path?: string; + items: NavLinkItem[]; } const Link = styled.a` @@ -50,7 +35,7 @@ const StyledEuiTitle = styled(EuiTitle)` export const LandingLinksIcons: React.FC = ({ items }) => ( - {items.map(({ label, description, path, id, icon }) => ( + {items.map(({ title, description, id, icon }) => ( = ({ items }) responsive={false} > - - - -

{label}

+ +

{title}

diff --git a/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_images.test.tsx b/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_images.test.tsx index 479de5e13f432..c44374852f29b 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_images.test.tsx +++ b/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_images.test.tsx @@ -8,14 +8,16 @@ import { render } from '@testing-library/react'; import React from 'react'; import { SecurityPageName } from '../../app/types'; +import { NavLinkItem } from '../../common/links/types'; import { TestProviders } from '../../common/mock'; -import { LandingLinksImages, NavItem } from './landing_links_images'; +import { LandingLinksImages } from './landing_links_images'; -const DEFAULT_NAV_ITEM: NavItem = { +const DEFAULT_NAV_ITEM: NavLinkItem = { id: SecurityPageName.overview, - label: 'TEST LABEL', + title: 'TEST LABEL', description: 'TEST DESCRIPTION', image: 'TEST_IMAGE.png', + path: '', }; jest.mock('../../common/lib/kibana/kibana_react', () => { @@ -32,24 +34,24 @@ jest.mock('../../common/lib/kibana/kibana_react', () => { describe('LandingLinksImages', () => { it('renders', () => { - const label = 'test label'; + const title = 'test label'; const { queryByText } = render( - + ); - expect(queryByText(label)).toBeInTheDocument(); + expect(queryByText(title)).toBeInTheDocument(); }); it('renders image', () => { const image = 'test_image.jpeg'; - const label = 'TEST_LABEL'; + const title = 'TEST_LABEL'; const { getByTestId } = render( - + ); diff --git a/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_images.tsx b/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_images.tsx index b6a16da8cdc82..22bcc0f1aa251 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_images.tsx +++ b/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_images.tsx @@ -7,19 +7,11 @@ import { EuiFlexGroup, EuiFlexItem, EuiImage, EuiPanel, EuiText, EuiTitle } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; -import { SecurityPageName } from '../../app/types'; import { withSecuritySolutionLink } from '../../common/components/links'; +import { NavLinkItem } from '../../common/links/types'; interface LandingLinksImagesProps { - items: NavItem[]; -} - -export interface NavItem { - id: SecurityPageName; - label: string; - image: string; - description: string; - path?: string; + items: NavLinkItem[]; } const PrimaryEuiTitle = styled(EuiTitle)` @@ -47,24 +39,26 @@ const Content = styled(EuiFlexItem)` export const LandingLinksImages: React.FC = ({ items }) => ( - {items.map(({ label, description, path, image, id }) => ( + {items.map(({ title, description, image, id }) => ( - + {/* Empty onClick is to force hover style on `EuiPanel` */} {}}> - + {image && ( + + )} -

{label}

+

{title}

{description} diff --git a/x-pack/plugins/security_solution/public/landing_pages/constants.ts b/x-pack/plugins/security_solution/public/landing_pages/constants.ts new file mode 100644 index 0000000000000..a6b72a5e7db4f --- /dev/null +++ b/x-pack/plugins/security_solution/public/landing_pages/constants.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { SecurityPageName } from '../app/types'; + +export interface LandingNavGroup { + label: string; + itemIds: SecurityPageName[]; +} + +export const MANAGE_NAVIGATION_CATEGORIES: LandingNavGroup[] = [ + { + label: i18n.translate('xpack.securitySolution.landing.siemTitle', { + defaultMessage: 'SIEM', + }), + itemIds: [SecurityPageName.rules, SecurityPageName.exceptions], + }, + { + label: i18n.translate('xpack.securitySolution.landing.endpointsTitle', { + defaultMessage: 'ENDPOINTS', + }), + itemIds: [ + SecurityPageName.endpoints, + SecurityPageName.policies, + SecurityPageName.trustedApps, + SecurityPageName.eventFilters, + SecurityPageName.blocklist, + SecurityPageName.hostIsolationExceptions, + ], + }, +]; diff --git a/x-pack/plugins/security_solution/public/landing_pages/pages/dashboards.tsx b/x-pack/plugins/security_solution/public/landing_pages/pages/dashboards.tsx index 8c49fda169ad3..c2d5a3d8d2ee9 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/pages/dashboards.tsx +++ b/x-pack/plugins/security_solution/public/landing_pages/pages/dashboards.tsx @@ -5,31 +5,24 @@ * 2.0. */ import React from 'react'; -import { i18n } from '@kbn/i18n'; import { SecurityPageName } from '../../app/types'; import { HeaderPage } from '../../common/components/header_page'; import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; import { SpyRoute } from '../../common/utils/route/spy_routes'; -import { LandingLinksImages, NavItem } from '../components/landing_links_images'; +import { LandingLinksImages } from '../components/landing_links_images'; import { DASHBOARDS_PAGE_TITLE } from './translations'; -import overviewPageImg from '../../common/images/overview_page.png'; -import { OVERVIEW } from '../../app/translations'; +import { useAppNavLinks } from '../../common/links'; -const items: NavItem[] = [ - { - id: SecurityPageName.overview, - label: OVERVIEW, - description: i18n.translate('xpack.securitySolution.landing.dashboards.overviewDescription', { - defaultMessage: 'What is going in your secuity environment', - }), - image: overviewPageImg, - }, -]; +export const DashboardsLandingPage = () => { + const dashboardlinks = useAppNavLinks().find( + ({ id }) => id === SecurityPageName.dashboardsLanding + ); -export const DashboardsLandingPage = () => ( - - - - - -); + return ( + + + + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/landing_pages/pages/manage.test.tsx b/x-pack/plugins/security_solution/public/landing_pages/pages/manage.test.tsx index efb1bcf35c39e..1b786912b7c6e 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/pages/manage.test.tsx +++ b/x-pack/plugins/security_solution/public/landing_pages/pages/manage.test.tsx @@ -9,43 +9,56 @@ import { render } from '@testing-library/react'; import React from 'react'; import { SecurityPageName } from '../../app/types'; import { TestProviders } from '../../common/mock'; -import { LandingCategories, NavConfigType } from './manage'; +import { LandingCategories } from './manage'; const RULES_ITEM_LABEL = 'elastic rules!'; const EXCEPTIONS_ITEM_LABEL = 'exceptional!'; +import { NavLinkItem } from '../../common/links/types'; -const testConfig: NavConfigType = { - categories: [ - { - label: 'first tests category', - itemIds: [SecurityPageName.rules], - }, - { - label: 'second tests category', - itemIds: [SecurityPageName.exceptions], - }, - ], - items: [ - { - id: SecurityPageName.rules, - label: RULES_ITEM_LABEL, - description: '', - icon: 'testIcon1', - }, - { - id: SecurityPageName.exceptions, - label: EXCEPTIONS_ITEM_LABEL, - description: '', - icon: 'testIcon2', - }, - ], -}; +const mockAppLinks: NavLinkItem[] = [ + { + id: SecurityPageName.administration, + path: '', + title: 'admin', + links: [ + { + id: SecurityPageName.rules, + title: RULES_ITEM_LABEL, + description: '', + icon: 'testIcon1', + path: '', + }, + { + id: SecurityPageName.exceptions, + title: EXCEPTIONS_ITEM_LABEL, + description: '', + icon: 'testIcon2', + path: '', + }, + ], + }, +]; + +jest.mock('../../common/links', () => ({ + useAppNavLinks: jest.fn(() => mockAppLinks), +})); describe('LandingCategories', () => { it('renders items', () => { const { queryByText } = render( - + ); @@ -57,15 +70,12 @@ describe('LandingCategories', () => { const { queryAllByTestId } = render( ); diff --git a/x-pack/plugins/security_solution/public/landing_pages/pages/manage.tsx b/x-pack/plugins/security_solution/public/landing_pages/pages/manage.tsx index da4d25f621305..172506868cec9 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/pages/manage.tsx +++ b/x-pack/plugins/security_solution/public/landing_pages/pages/manage.tsx @@ -5,140 +5,24 @@ * 2.0. */ import { EuiHorizontalRule, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import { compact } from 'lodash/fp'; import React from 'react'; import styled from 'styled-components'; -import { - BLOCKLIST, - ENDPOINTS, - EVENT_FILTERS, - EXCEPTIONS, - TRUSTED_APPLICATIONS, -} from '../../app/translations'; + import { SecurityPageName } from '../../app/types'; import { HeaderPage } from '../../common/components/header_page'; import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; +import { useAppNavLinks } from '../../common/links'; +import { NavLinkItem } from '../../common/links/types'; import { SpyRoute } from '../../common/utils/route/spy_routes'; -import { LandingLinksIcons, NavItem } from '../components/landing_links_icons'; -import { IconBlocklist } from '../icons/blocklist'; -import { IconEndpoints } from '../icons/endpoints'; -import { IconEndpointPolicies } from '../icons/endpoint_policies'; -import { IconEventFilters } from '../icons/event_filters'; -import { IconExceptionLists } from '../icons/exception_lists'; -import { IconHostIsolation } from '../icons/host_isolation'; -import { IconSiemRules } from '../icons/siem_rules'; -import { IconTrustedApplications } from '../icons/trusted_applications'; +import { LandingLinksIcons } from '../components/landing_links_icons'; +import { LandingNavGroup, MANAGE_NAVIGATION_CATEGORIES } from '../constants'; import { MANAGE_PAGE_TITLE } from './translations'; -// TODO -const FIX_ME_TEMPORARY_DESCRIPTION = 'Description here'; - -export interface NavConfigType { - items: NavItem[]; - categories: Array<{ label: string; itemIds: SecurityPageName[] }>; -} - -const config: NavConfigType = { - categories: [ - { - label: i18n.translate('xpack.securitySolution.landing.threatHunting.siemTitle', { - defaultMessage: 'SIEM', - }), - itemIds: [SecurityPageName.rules, SecurityPageName.exceptions], - }, - { - label: i18n.translate('xpack.securitySolution.landing.threatHunting.endpointsTitle', { - defaultMessage: 'ENDPOINTS', - }), - itemIds: [ - SecurityPageName.endpoints, - SecurityPageName.policies, - SecurityPageName.trustedApps, - SecurityPageName.eventFilters, - SecurityPageName.blocklist, - SecurityPageName.hostIsolationExceptions, - ], - }, - ], - items: [ - { - id: SecurityPageName.rules, - label: i18n.translate('xpack.securitySolution.landing.manage.rulesLabel', { - defaultMessage: 'SIEM rules', - }), - description: FIX_ME_TEMPORARY_DESCRIPTION, - icon: IconSiemRules, - }, - { - id: SecurityPageName.exceptions, - label: EXCEPTIONS, - description: FIX_ME_TEMPORARY_DESCRIPTION, - icon: IconExceptionLists, - }, - { - id: SecurityPageName.endpoints, - label: ENDPOINTS, - description: i18n.translate('xpack.securitySolution.landing.manage.endpointsDescription', { - defaultMessage: 'Hosts running endpoint security', - }), - icon: IconEndpoints, - }, - { - id: SecurityPageName.policies, - label: i18n.translate('xpack.securitySolution.landing.manage.endpointPoliceLabel', { - defaultMessage: 'Endpoint policies', - }), - description: FIX_ME_TEMPORARY_DESCRIPTION, - icon: IconEndpointPolicies, - }, - { - id: SecurityPageName.trustedApps, - label: TRUSTED_APPLICATIONS, - description: i18n.translate( - 'xpack.securitySolution.landing.manage.trustedApplicationsDescription', - { - defaultMessage: - 'Improve performance or alleviate conflicts with other applications running on your hosts', - } - ), - icon: IconTrustedApplications, - }, - { - id: SecurityPageName.eventFilters, - label: EVENT_FILTERS, - description: i18n.translate('xpack.securitySolution.landing.manage.eventFiltersDescription', { - defaultMessage: 'Exclude unwanted applications from running on your hosts', - }), - icon: IconEventFilters, - }, - { - id: SecurityPageName.blocklist, - label: BLOCKLIST, - description: FIX_ME_TEMPORARY_DESCRIPTION, - icon: IconBlocklist, - }, - { - id: SecurityPageName.hostIsolationExceptions, - label: i18n.translate('xpack.securitySolution.landing.manage.hostIsolationLabel', { - defaultMessage: 'Host isolation IP exceptions', - }), - description: i18n.translate( - 'xpack.securitySolution.landing.manage.hostIsolationDescription', - { - defaultMessage: 'Allow isolated hosts to communicate with specific IPs', - } - ), - - icon: IconHostIsolation, - }, - ], -}; - export const ManageLandingPage = () => ( - + ); @@ -148,16 +32,18 @@ const StyledEuiHorizontalRule = styled(EuiHorizontalRule)` margin-bottom: ${({ theme }) => theme.eui.paddingSizes.l}; `; -const getNavItembyId = (navConfig: NavConfigType) => (itemId: string) => - navConfig.items.find(({ id }: NavItem) => id === itemId); +const getNavItembyId = (links: NavLinkItem[]) => (itemId: string) => + links.find(({ id }: NavLinkItem) => id === itemId); + +const navItemsFromIds = (itemIds: SecurityPageName[], links: NavLinkItem[]) => + compact(itemIds.map(getNavItembyId(links))); -const navItemsFromIds = (itemIds: SecurityPageName[], navConfig: NavConfigType) => - compact(itemIds.map(getNavItembyId(navConfig))); +export const LandingCategories = React.memo(({ groups }: { groups: LandingNavGroup[] }) => { + const manageLink = useAppNavLinks().find(({ id }) => id === SecurityPageName.administration); -export const LandingCategories = React.memo(({ navConfig }: { navConfig: NavConfigType }) => { return ( <> - {navConfig.categories.map(({ label, itemIds }, index) => ( + {groups.map(({ label, itemIds }, index) => (
{index > 0 && ( <> @@ -169,7 +55,7 @@ export const LandingCategories = React.memo(({ navConfig }: { navConfig: NavConf

{label}

- +
))} diff --git a/x-pack/plugins/security_solution/public/landing_pages/pages/threat_hunting.tsx b/x-pack/plugins/security_solution/public/landing_pages/pages/threat_hunting.tsx index 2a0f4e471a75d..7d486c102a2dd 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/pages/threat_hunting.tsx +++ b/x-pack/plugins/security_solution/public/landing_pages/pages/threat_hunting.tsx @@ -5,51 +5,23 @@ * 2.0. */ import React from 'react'; -import { i18n } from '@kbn/i18n'; import { SecurityPageName } from '../../app/types'; import { HeaderPage } from '../../common/components/header_page'; import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; import { SpyRoute } from '../../common/utils/route/spy_routes'; -import { LandingLinksImages, NavItem } from '../components/landing_links_images'; +import { LandingLinksImages } from '../components/landing_links_images'; import { THREAT_HUNTING_PAGE_TITLE } from './translations'; -import userPageImg from '../../common/images/users_page.png'; -import hostsPageImg from '../../common/images/hosts_page.png'; -import networkPageImg from '../../common/images/network_page.png'; -import { HOSTS, NETWORK, USERS } from '../../app/translations'; +import { useAppNavLinks } from '../../common/links'; -const items: NavItem[] = [ - { - id: SecurityPageName.hosts, - label: HOSTS, - description: i18n.translate('xpack.securitySolution.landing.threatHunting.hostsDescription', { - defaultMessage: - 'Computer or other device that communicates with other hosts on a network. Hosts on a network include clients and servers -- that send or receive data, services or applications.', - }), - image: hostsPageImg, - }, - { - id: SecurityPageName.network, - label: NETWORK, - description: i18n.translate('xpack.securitySolution.landing.threatHunting.networkDescription', { - defaultMessage: - 'The action or process of interacting with others to exchange information and develop professional or social contacts.', - }), - image: networkPageImg, - }, - { - id: SecurityPageName.users, - label: USERS, - description: i18n.translate('xpack.securitySolution.landing.threatHunting.usersDescription', { - defaultMessage: 'Sudo commands dashboard from the Logs System integration.', - }), - image: userPageImg, - }, -]; - -export const ThreatHuntingLandingPage = () => ( - - - - - -); +export const ThreatHuntingLandingPage = () => { + const threatHuntinglinks = useAppNavLinks().find( + ({ id }) => id === SecurityPageName.threatHuntingLanding + ); + return ( + + + + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/landing_pages/icons/blocklist.tsx b/x-pack/plugins/security_solution/public/management/icons/blocklist.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/landing_pages/icons/blocklist.tsx rename to x-pack/plugins/security_solution/public/management/icons/blocklist.tsx diff --git a/x-pack/plugins/security_solution/public/landing_pages/icons/endpoint_policies.tsx b/x-pack/plugins/security_solution/public/management/icons/endpoint_policies.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/landing_pages/icons/endpoint_policies.tsx rename to x-pack/plugins/security_solution/public/management/icons/endpoint_policies.tsx diff --git a/x-pack/plugins/security_solution/public/landing_pages/icons/endpoints.tsx b/x-pack/plugins/security_solution/public/management/icons/endpoints.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/landing_pages/icons/endpoints.tsx rename to x-pack/plugins/security_solution/public/management/icons/endpoints.tsx diff --git a/x-pack/plugins/security_solution/public/landing_pages/icons/event_filters.tsx b/x-pack/plugins/security_solution/public/management/icons/event_filters.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/landing_pages/icons/event_filters.tsx rename to x-pack/plugins/security_solution/public/management/icons/event_filters.tsx diff --git a/x-pack/plugins/security_solution/public/landing_pages/icons/exception_lists.tsx b/x-pack/plugins/security_solution/public/management/icons/exception_lists.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/landing_pages/icons/exception_lists.tsx rename to x-pack/plugins/security_solution/public/management/icons/exception_lists.tsx diff --git a/x-pack/plugins/security_solution/public/landing_pages/icons/host_isolation.tsx b/x-pack/plugins/security_solution/public/management/icons/host_isolation.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/landing_pages/icons/host_isolation.tsx rename to x-pack/plugins/security_solution/public/management/icons/host_isolation.tsx diff --git a/x-pack/plugins/security_solution/public/landing_pages/icons/siem_rules.tsx b/x-pack/plugins/security_solution/public/management/icons/siem_rules.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/landing_pages/icons/siem_rules.tsx rename to x-pack/plugins/security_solution/public/management/icons/siem_rules.tsx diff --git a/x-pack/plugins/security_solution/public/landing_pages/icons/trusted_applications.tsx b/x-pack/plugins/security_solution/public/management/icons/trusted_applications.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/landing_pages/icons/trusted_applications.tsx rename to x-pack/plugins/security_solution/public/management/icons/trusted_applications.tsx diff --git a/x-pack/plugins/security_solution/public/management/links.ts b/x-pack/plugins/security_solution/public/management/links.ts index d941d538c80f7..98a78820c4b84 100644 --- a/x-pack/plugins/security_solution/public/management/links.ts +++ b/x-pack/plugins/security_solution/public/management/links.ts @@ -31,6 +31,16 @@ import { } from '../app/translations'; import { FEATURE, LinkItem } from '../common/links/types'; +import { IconBlocklist } from './icons/blocklist'; +import { IconEndpoints } from './icons/endpoints'; +import { IconEndpointPolicies } from './icons/endpoint_policies'; +import { IconEventFilters } from './icons/event_filters'; +import { IconExceptionLists } from './icons/exception_lists'; +import { IconHostIsolation } from './icons/host_isolation'; +import { IconSiemRules } from './icons/siem_rules'; +import { IconTrustedApplications } from './icons/trusted_applications'; +const FIX_ME_TEMPORARY_DESCRIPTION = 'Description here'; + export const links: LinkItem = { id: SecurityPageName.administration, title: MANAGE, @@ -47,6 +57,12 @@ export const links: LinkItem = { { id: SecurityPageName.rules, title: RULES, + description: i18n.translate('xpack.securitySolution.appLinks.rulesDescription', { + defaultMessage: + "Create and manage rules to check for suspicious source events, and create alerts when a rule's conditions are met.", + }), + + landingIcon: IconSiemRules, path: RULES_PATH, globalNavEnabled: false, globalSearchKeywords: [ @@ -59,6 +75,10 @@ export const links: LinkItem = { { id: SecurityPageName.exceptions, title: EXCEPTIONS, + description: i18n.translate('xpack.securitySolution.appLinks.exceptionsDescription', { + defaultMessage: 'Create and manage exceptions to prevent the creation of unwanted alerts.', + }), + landingIcon: IconExceptionLists, path: EXCEPTIONS_PATH, globalNavEnabled: false, globalSearchKeywords: [ @@ -70,6 +90,10 @@ export const links: LinkItem = { }, { id: SecurityPageName.endpoints, + description: i18n.translate('xpack.securitySolution.appLinks.endpointsDescription', { + defaultMessage: 'Hosts running endpoint security', + }), + landingIcon: IconEndpoints, globalNavEnabled: true, title: ENDPOINTS, globalNavOrder: 9006, @@ -79,6 +103,11 @@ export const links: LinkItem = { { id: SecurityPageName.policies, title: POLICIES, + description: i18n.translate('xpack.securitySolution.appLinks.policiesDescription', { + defaultMessage: + 'Use policies to customize endpoint and cloud workload protections and other configurations', + }), + landingIcon: IconEndpointPolicies, path: POLICIES_PATH, skipUrlState: true, experimentalKey: 'policyListEnabled', @@ -86,24 +115,42 @@ export const links: LinkItem = { { id: SecurityPageName.trustedApps, title: TRUSTED_APPLICATIONS, + description: i18n.translate( + 'xpack.securitySolution.appLinks.trustedApplicationsDescription', + { + defaultMessage: + 'Improve performance or alleviate conflicts with other applications running on your hosts', + } + ), + landingIcon: IconTrustedApplications, path: TRUSTED_APPS_PATH, skipUrlState: true, }, { id: SecurityPageName.eventFilters, title: EVENT_FILTERS, + description: i18n.translate('xpack.securitySolution.appLinks.eventFiltersDescription', { + defaultMessage: 'Exclude unwanted applications from running on your hosts', + }), + landingIcon: IconEventFilters, path: EVENT_FILTERS_PATH, skipUrlState: true, }, { id: SecurityPageName.hostIsolationExceptions, title: HOST_ISOLATION_EXCEPTIONS, + description: i18n.translate('xpack.securitySolution.appLinks.hostIsolationDescription', { + defaultMessage: 'Allow isolated hosts to communicate with specific IPs', + }), + landingIcon: IconHostIsolation, path: HOST_ISOLATION_EXCEPTIONS_PATH, skipUrlState: true, }, { id: SecurityPageName.blocklist, title: BLOCKLIST, + description: FIX_ME_TEMPORARY_DESCRIPTION, + landingIcon: IconBlocklist, path: BLOCKLIST_PATH, skipUrlState: true, }, diff --git a/x-pack/plugins/security_solution/public/network/links.ts b/x-pack/plugins/security_solution/public/network/links.ts index ad209a220eebc..f52a0c6a11754 100644 --- a/x-pack/plugins/security_solution/public/network/links.ts +++ b/x-pack/plugins/security_solution/public/network/links.ts @@ -9,10 +9,16 @@ import { i18n } from '@kbn/i18n'; import { NETWORK_PATH, SecurityPageName } from '../../common/constants'; import { NETWORK } from '../app/translations'; import { LinkItem } from '../common/links/types'; +import networkPageImg from '../common/images/network_page.png'; export const links: LinkItem = { id: SecurityPageName.network, title: NETWORK, + landingImage: networkPageImg, + description: i18n.translate('xpack.securitySolution.appLinks.network.description', { + defaultMessage: + 'The action or process of interacting with others to exchange information and develop professional or social contacts.', + }), path: NETWORK_PATH, globalNavEnabled: true, globalSearchKeywords: [ diff --git a/x-pack/plugins/security_solution/public/overview/links.ts b/x-pack/plugins/security_solution/public/overview/links.ts index 89f75053b3d6f..2cfb8df582742 100644 --- a/x-pack/plugins/security_solution/public/overview/links.ts +++ b/x-pack/plugins/security_solution/public/overview/links.ts @@ -15,10 +15,16 @@ import { } from '../../common/constants'; import { DASHBOARDS, DETECTION_RESPONSE, GETTING_STARTED, OVERVIEW } from '../app/translations'; import { FEATURE, LinkItem } from '../common/links/types'; +import overviewPageImg from '../common/images/overview_page.png'; +import detectionResponsePageImg from '../common/images/detection_response_page.png'; export const overviewLinks: LinkItem = { id: SecurityPageName.overview, title: OVERVIEW, + landingImage: overviewPageImg, + description: i18n.translate('xpack.securitySolution.appLinks.overviewDescription', { + defaultMessage: 'What is going in your secuity environment', + }), path: OVERVIEW_PATH, globalNavEnabled: true, features: [FEATURE.general], @@ -47,6 +53,11 @@ export const gettingStartedLinks: LinkItem = { export const detectionResponseLinks: LinkItem = { id: SecurityPageName.detectionAndResponse, title: DETECTION_RESPONSE, + landingImage: detectionResponsePageImg, + description: i18n.translate('xpack.securitySolution.appLinks.detectionAndResponseDescription', { + defaultMessage: + "Monitor the impact of application and device performance from the end user's point of view.", + }), path: DETECTION_RESPONSE_PATH, globalNavEnabled: false, experimentalKey: 'detectionResponseEnabled', diff --git a/x-pack/plugins/security_solution/public/users/links.ts b/x-pack/plugins/security_solution/public/users/links.ts index bd7bef4af8e82..c7176ec11daef 100644 --- a/x-pack/plugins/security_solution/public/users/links.ts +++ b/x-pack/plugins/security_solution/public/users/links.ts @@ -9,10 +9,15 @@ import { i18n } from '@kbn/i18n'; import { SecurityPageName, USERS_PATH } from '../../common/constants'; import { USERS } from '../app/translations'; import { LinkItem } from '../common/links/types'; +import userPageImg from '../common/images/users_page.png'; export const links: LinkItem = { id: SecurityPageName.users, title: USERS, + landingImage: userPageImg, + description: i18n.translate('xpack.securitySolution.appLinks.users.description', { + defaultMessage: 'Sudo commands dashboard from the Logs System integration.', + }), path: USERS_PATH, globalNavEnabled: true, experimentalKey: 'usersEnabled', From cda1850b0188e2fb0912a918da19d7d250ec3542 Mon Sep 17 00:00:00 2001 From: semd Date: Fri, 13 May 2022 12:45:46 +0200 Subject: [PATCH 02/25] align app links changes --- .../components/navigation/nav_links.test.ts | 54 ++++++++++++++++ .../common/components/navigation/nav_links.ts | 25 ++++++++ .../common/components/navigation/types.ts | 7 ++ .../public/common/links/links.ts | 11 ---- .../public/landing_pages/pages/dashboards.tsx | 8 +-- .../landing_pages/pages/manage.test.tsx | 61 +++++++++--------- .../public/landing_pages/pages/manage.tsx | 64 ++++++++++--------- .../landing_pages/pages/threat_hunting.tsx | 9 ++- .../public/management/links.ts | 23 +++++++ 9 files changed, 178 insertions(+), 84 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/navigation/nav_links.test.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/navigation/nav_links.ts diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/nav_links.test.ts b/x-pack/plugins/security_solution/public/common/components/navigation/nav_links.test.ts new file mode 100644 index 0000000000000..ff7aa7581fc4b --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/navigation/nav_links.test.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { SecurityPageName } from '../../../app/types'; +import { NavLinkItem } from '../../links/types'; +import { TestProviders } from '../../mock'; +import { useAppNavLinks, useAppRootNavLink } from './nav_links'; + +const mockNavLinks = [ + { + description: 'description', + id: SecurityPageName.administration, + links: [ + { + description: 'description 2', + id: SecurityPageName.endpoints, + links: [], + path: '/path_2', + title: 'title 2', + }, + ], + path: '/path', + title: 'title', + }, +]; + +jest.mock('../../links', () => ({ + getNavLinkItems: () => mockNavLinks, +})); + +const renderUseAppNavLinks = () => + renderHook<{}, NavLinkItem[]>(() => useAppNavLinks(), { wrapper: TestProviders }); + +const renderUseAppRootNavLink = (id: SecurityPageName) => + renderHook<{ id: SecurityPageName }, NavLinkItem | undefined>(() => useAppRootNavLink(id), { + wrapper: TestProviders, + }); + +describe('useAppNavLinks', () => { + it('should return all nav links', () => { + const { result } = renderUseAppNavLinks(); + expect(result.current).toEqual(mockNavLinks); + }); + + it('should return a root nav links', () => { + const { result } = renderUseAppRootNavLink(SecurityPageName.administration); + expect(result.current).toEqual(mockNavLinks[0]); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/nav_links.ts b/x-pack/plugins/security_solution/public/common/components/navigation/nav_links.ts new file mode 100644 index 0000000000000..efdf72a1f7926 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/navigation/nav_links.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useKibana } from '../../lib/kibana'; +import { useEnableExperimental } from '../../hooks/use_experimental_features'; +import { useLicense } from '../../hooks/use_license'; +import { getNavLinkItems } from '../../links'; +import type { SecurityPageName } from '../../../app/types'; +import type { NavLinkItem } from '../../links/types'; + +export const useAppNavLinks = (): NavLinkItem[] => { + const license = useLicense(); + const enableExperimental = useEnableExperimental(); + const capabilities = useKibana().services.application.capabilities; + + return getNavLinkItems({ enableExperimental, license, capabilities }); +}; + +export const useAppRootNavLink = (linkId: SecurityPageName): NavLinkItem | undefined => { + return useAppNavLinks().find(({ id }) => id === linkId); +}; 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 bc20a98eae1e8..91edd1feea2da 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 @@ -76,3 +76,10 @@ export type GetUrlForApp = ( ) => string; export type NavigateToUrl = (url: string) => void; + +export interface NavigationCategory { + label: string; + linkIds: readonly SecurityPageName[]; +} + +export type NavigationCategories = Readonly; diff --git a/x-pack/plugins/security_solution/public/common/links/links.ts b/x-pack/plugins/security_solution/public/common/links/links.ts index a150ab2e7e0ce..af9357a122a1e 100644 --- a/x-pack/plugins/security_solution/public/common/links/links.ts +++ b/x-pack/plugins/security_solution/public/common/links/links.ts @@ -8,9 +8,6 @@ import { AppDeepLink, AppNavLinkStatus, Capabilities } from '@kbn/core/public'; import { get } from 'lodash'; import { SecurityPageName } from '../../../common/constants'; -import { useEnableExperimental } from '../hooks/use_experimental_features'; -import { useLicense } from '../hooks/use_license'; -import { useKibana } from '../lib/kibana'; import { appLinks, getAppLinks } from './app_links'; import { Feature, @@ -196,11 +193,3 @@ export const getAncestorLinksInfo = (id: SecurityPageName): LinkInfo[] => { export const needsUrlState = (id: SecurityPageName): boolean => { return !getNormalizedLink(id).skipUrlState; }; - -export const useAppNavLinks = (): NavLinkItem[] => { - const license = useLicense(); - const enableExperimental = useEnableExperimental(); - const capabilities = useKibana().services.application.capabilities; - - return getNavLinkItems({ enableExperimental, license, capabilities }); -}; diff --git a/x-pack/plugins/security_solution/public/landing_pages/pages/dashboards.tsx b/x-pack/plugins/security_solution/public/landing_pages/pages/dashboards.tsx index c2d5a3d8d2ee9..1d46aa6706a26 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/pages/dashboards.tsx +++ b/x-pack/plugins/security_solution/public/landing_pages/pages/dashboards.tsx @@ -7,21 +7,19 @@ import React from 'react'; import { SecurityPageName } from '../../app/types'; import { HeaderPage } from '../../common/components/header_page'; +import { useAppRootNavLink } from '../../common/components/navigation/nav_links'; import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { LandingLinksImages } from '../components/landing_links_images'; import { DASHBOARDS_PAGE_TITLE } from './translations'; -import { useAppNavLinks } from '../../common/links'; export const DashboardsLandingPage = () => { - const dashboardlinks = useAppNavLinks().find( - ({ id }) => id === SecurityPageName.dashboardsLanding - ); + const dashboardLinks = useAppRootNavLink(SecurityPageName.dashboardsLanding)?.links ?? []; return ( - + ); diff --git a/x-pack/plugins/security_solution/public/landing_pages/pages/manage.test.tsx b/x-pack/plugins/security_solution/public/landing_pages/pages/manage.test.tsx index 1b786912b7c6e..1955d56c0a151 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/pages/manage.test.tsx +++ b/x-pack/plugins/security_solution/public/landing_pages/pages/manage.test.tsx @@ -10,37 +10,34 @@ import React from 'react'; import { SecurityPageName } from '../../app/types'; import { TestProviders } from '../../common/mock'; import { LandingCategories } from './manage'; +import { NavLinkItem } from '../../common/links/types'; const RULES_ITEM_LABEL = 'elastic rules!'; const EXCEPTIONS_ITEM_LABEL = 'exceptional!'; -import { NavLinkItem } from '../../common/links/types'; - -const mockAppLinks: NavLinkItem[] = [ - { - id: SecurityPageName.administration, - path: '', - title: 'admin', - links: [ - { - id: SecurityPageName.rules, - title: RULES_ITEM_LABEL, - description: '', - icon: 'testIcon1', - path: '', - }, - { - id: SecurityPageName.exceptions, - title: EXCEPTIONS_ITEM_LABEL, - description: '', - icon: 'testIcon2', - path: '', - }, - ], - }, -]; -jest.mock('../../common/links', () => ({ - useAppNavLinks: jest.fn(() => mockAppLinks), +const mockAppManageLink: NavLinkItem = { + id: SecurityPageName.administration, + path: '', + title: 'admin', + links: [ + { + id: SecurityPageName.rules, + title: RULES_ITEM_LABEL, + description: '', + icon: 'testIcon1', + path: '', + }, + { + id: SecurityPageName.exceptions, + title: EXCEPTIONS_ITEM_LABEL, + description: '', + icon: 'testIcon2', + path: '', + }, + ], +}; +jest.mock('../../common/components/navigation/nav_links', () => ({ + useAppRootNavLink: jest.fn(() => mockAppManageLink), })); describe('LandingCategories', () => { @@ -48,14 +45,14 @@ describe('LandingCategories', () => { const { queryByText } = render( @@ -70,10 +67,10 @@ describe('LandingCategories', () => { const { queryAllByTestId } = render( diff --git a/x-pack/plugins/security_solution/public/landing_pages/pages/manage.tsx b/x-pack/plugins/security_solution/public/landing_pages/pages/manage.tsx index 172506868cec9..f0e6094d5113f 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/pages/manage.tsx +++ b/x-pack/plugins/security_solution/public/landing_pages/pages/manage.tsx @@ -5,24 +5,23 @@ * 2.0. */ import { EuiHorizontalRule, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { compact } from 'lodash/fp'; import React from 'react'; import styled from 'styled-components'; import { SecurityPageName } from '../../app/types'; import { HeaderPage } from '../../common/components/header_page'; +import { useAppRootNavLink } from '../../common/components/navigation/nav_links'; +import { NavigationCategories } from '../../common/components/navigation/types'; import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; -import { useAppNavLinks } from '../../common/links'; -import { NavLinkItem } from '../../common/links/types'; import { SpyRoute } from '../../common/utils/route/spy_routes'; +import { navigationCategories } from '../../management/links'; import { LandingLinksIcons } from '../components/landing_links_icons'; -import { LandingNavGroup, MANAGE_NAVIGATION_CATEGORIES } from '../constants'; import { MANAGE_PAGE_TITLE } from './translations'; export const ManageLandingPage = () => ( - + ); @@ -32,34 +31,37 @@ const StyledEuiHorizontalRule = styled(EuiHorizontalRule)` margin-bottom: ${({ theme }) => theme.eui.paddingSizes.l}; `; -const getNavItembyId = (links: NavLinkItem[]) => (itemId: string) => - links.find(({ id }: NavLinkItem) => id === itemId); +const useGetManageNavLinks = () => { + const manageNavLinks = useAppRootNavLink(SecurityPageName.administration)?.links ?? []; -const navItemsFromIds = (itemIds: SecurityPageName[], links: NavLinkItem[]) => - compact(itemIds.map(getNavItembyId(links))); + const manageLinksById = Object.fromEntries(manageNavLinks.map((link) => [link.id, link])); + return (linkIds: readonly SecurityPageName[]) => linkIds.map((linkId) => manageLinksById[linkId]); +}; -export const LandingCategories = React.memo(({ groups }: { groups: LandingNavGroup[] }) => { - const manageLink = useAppNavLinks().find(({ id }) => id === SecurityPageName.administration); +export const LandingCategories = React.memo( + ({ categories }: { categories: NavigationCategories }) => { + const getManageNavLinks = useGetManageNavLinks(); - return ( - <> - {groups.map(({ label, itemIds }, index) => ( -
- {index > 0 && ( - <> - - - - )} - -

{label}

-
- - -
- ))} - - ); -}); + return ( + <> + {categories.map(({ label, linkIds }, index) => ( +
+ {index > 0 && ( + <> + + + + )} + +

{label}

+
+ + +
+ ))} + + ); + } +); LandingCategories.displayName = 'LandingCategories'; diff --git a/x-pack/plugins/security_solution/public/landing_pages/pages/threat_hunting.tsx b/x-pack/plugins/security_solution/public/landing_pages/pages/threat_hunting.tsx index 7d486c102a2dd..605a1baeedbd6 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/pages/threat_hunting.tsx +++ b/x-pack/plugins/security_solution/public/landing_pages/pages/threat_hunting.tsx @@ -7,20 +7,19 @@ import React from 'react'; import { SecurityPageName } from '../../app/types'; import { HeaderPage } from '../../common/components/header_page'; +import { useAppRootNavLink } from '../../common/components/navigation/nav_links'; import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { LandingLinksImages } from '../components/landing_links_images'; import { THREAT_HUNTING_PAGE_TITLE } from './translations'; -import { useAppNavLinks } from '../../common/links'; export const ThreatHuntingLandingPage = () => { - const threatHuntinglinks = useAppNavLinks().find( - ({ id }) => id === SecurityPageName.threatHuntingLanding - ); + const threatHuntinglinks = useAppRootNavLink(SecurityPageName.threatHuntingLanding)?.links ?? []; + return ( - + ); diff --git a/x-pack/plugins/security_solution/public/management/links.ts b/x-pack/plugins/security_solution/public/management/links.ts index 98a78820c4b84..1e26f0e5ca6e1 100644 --- a/x-pack/plugins/security_solution/public/management/links.ts +++ b/x-pack/plugins/security_solution/public/management/links.ts @@ -29,6 +29,7 @@ import { RULES, TRUSTED_APPLICATIONS, } from '../app/translations'; +import { NavigationCategories } from '../common/components/navigation/types'; import { FEATURE, LinkItem } from '../common/links/types'; import { IconBlocklist } from './icons/blocklist'; @@ -156,3 +157,25 @@ export const links: LinkItem = { }, ], }; + +export const navigationCategories: NavigationCategories = [ + { + label: i18n.translate('xpack.securitySolution.appLinks.category.siem', { + defaultMessage: 'SIEM', + }), + linkIds: [SecurityPageName.rules, SecurityPageName.exceptions], + }, + { + label: i18n.translate('xpack.securitySolution.appLinks.category.endpoints', { + defaultMessage: 'ENDPOINTS', + }), + linkIds: [ + SecurityPageName.endpoints, + SecurityPageName.policies, + SecurityPageName.trustedApps, + SecurityPageName.eventFilters, + SecurityPageName.blocklist, + SecurityPageName.hostIsolationExceptions, + ], + }, +] as const; From 8df2b1b41a334a2e1c3b06c2f6646e58fc28630f Mon Sep 17 00:00:00 2001 From: semd Date: Fri, 13 May 2022 12:51:31 +0200 Subject: [PATCH 03/25] link configs refactor to use updater$ --- .../public/app/deep_links/index.ts | 39 +++- .../security_solution/public/cases/links.ts | 10 +- .../public/common/links/app_links.ts | 38 +--- .../public/common/links/links.ts | 184 ++++++++---------- .../public/common/links/types.ts | 30 ++- .../public/detections/links.ts | 6 +- .../public/landing_pages/links.ts | 48 +++++ .../public/management/links.ts | 5 +- .../public/overview/links.ts | 26 +-- .../security_solution/public/plugin.tsx | 35 ++-- .../public/timelines/links.ts | 6 +- 11 files changed, 219 insertions(+), 208 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/landing_pages/links.ts diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.ts index e9735b8c0b903..579ebbc9e3f3c 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.ts @@ -10,7 +10,8 @@ import { i18n } from '@kbn/i18n'; import { get } from 'lodash'; import { LicenseType } from '@kbn/licensing-plugin/common/types'; import { getCasesDeepLinks } from '@kbn/cases-plugin/public'; -import { AppDeepLink, AppNavLinkStatus, Capabilities } from '@kbn/core/public'; +import { AppDeepLink, AppNavLinkStatus, AppUpdater, Capabilities } from '@kbn/core/public'; +import { Subject } from 'rxjs'; import { SecurityPageName } from '../types'; import { OVERVIEW, @@ -60,6 +61,8 @@ import { DASHBOARDS_PATH, } from '../../../common/constants'; import { ExperimentalFeatures } from '../../../common/experimental_features'; +import { getAllAppLinks, appLinksUpdater$ } from '../../common/links'; +import { LinkItem } from '../../common/links/types'; const FEATURE = { general: `${SERVER_APP_ID}.show`, @@ -541,3 +544,37 @@ export function isPremiumLicense(licenseType?: LicenseType): boolean { licenseType === 'trial' ); } + +/** + * New deep links code starts here. + * All the code above will be removed once the appLinks migration is over. + * The code below manages the new implementation using the unified appLinks. + */ + +// Returns all deep links without filtering, for the initial application register +export const getAllDeepLinks = (): AppDeepLink[] => formatDeepLinks(getAllAppLinks()); + +const formatDeepLinks = (appLinks: readonly LinkItem[]): AppDeepLink[] => + appLinks.map((appLink) => ({ + id: appLink.id, + path: appLink.path, + title: appLink.title, + navLinkStatus: appLink.globalNavEnabled ? AppNavLinkStatus.visible : AppNavLinkStatus.hidden, + ...(appLink.globalSearchKeywords != null ? { keywords: appLink.globalSearchKeywords } : {}), + ...(appLink.globalNavOrder != null ? { order: appLink.globalNavOrder } : {}), + ...(appLink.globalSearchEnabled != null ? { searchable: appLink.globalSearchEnabled } : {}), + ...(appLink.links && appLink.links?.length + ? { + deepLinks: formatDeepLinks(appLink.links), + } + : {}), + })); + +export const registerDeepLinksUpdater = (appUpdater$: Subject) => { + appLinksUpdater$.subscribe((appLinks) => { + appUpdater$.next(() => ({ + navLinkStatus: AppNavLinkStatus.hidden, // needed to prevent main security link to switch to visible after update + deepLinks: formatDeepLinks(appLinks), + })); + }); +}; diff --git a/x-pack/plugins/security_solution/public/cases/links.ts b/x-pack/plugins/security_solution/public/cases/links.ts index 3765dfadc8fcc..3484454a4f02b 100644 --- a/x-pack/plugins/security_solution/public/cases/links.ts +++ b/x-pack/plugins/security_solution/public/cases/links.ts @@ -6,8 +6,8 @@ */ import { getCasesDeepLinks } from '@kbn/cases-plugin/public'; -import { CASES_PATH, SecurityPageName } from '../../common/constants'; -import { FEATURE, LinkItem } from '../common/links/types'; +import { CASES_FEATURE_ID, CASES_PATH, SecurityPageName } from '../../common/constants'; +import { LinkItem } from '../common/links/types'; export const getCasesLinkItems = (): LinkItem => { const casesLinks = getCasesDeepLinks({ @@ -16,14 +16,14 @@ export const getCasesLinkItems = (): LinkItem => { [SecurityPageName.case]: { globalNavEnabled: true, globalNavOrder: 9006, - features: [FEATURE.casesRead], + capabilities: [`${CASES_FEATURE_ID}.read_cases`], }, [SecurityPageName.caseConfigure]: { - features: [FEATURE.casesCrud], + capabilities: [`${CASES_FEATURE_ID}.crud_cases`], licenseType: 'gold', }, [SecurityPageName.caseCreate]: { - features: [FEATURE.casesCrud], + capabilities: [`${CASES_FEATURE_ID}.crud_cases`], }, }, }); diff --git a/x-pack/plugins/security_solution/public/common/links/app_links.ts b/x-pack/plugins/security_solution/public/common/links/app_links.ts index 4a972bd5deb1f..a1b134e63b4bc 100644 --- a/x-pack/plugins/security_solution/public/common/links/app_links.ts +++ b/x-pack/plugins/security_solution/public/common/links/app_links.ts @@ -4,46 +4,20 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { i18n } from '@kbn/i18n'; -import { SecurityPageName, THREAT_HUNTING_PATH } from '../../../common/constants'; -import { THREAT_HUNTING } from '../../app/translations'; -import { FEATURE, LinkItem, UserPermissions } from './types'; -import { links as hostsLinks } from '../../hosts/links'; +import { AppLinkItems } from './types'; import { links as detectionLinks } from '../../detections/links'; -import { links as networkLinks } from '../../network/links'; -import { links as usersLinks } from '../../users/links'; import { links as timelinesLinks } from '../../timelines/links'; import { getCasesLinkItems } from '../../cases/links'; import { links as managementLinks } from '../../management/links'; -import { gettingStartedLinks, dashboardsLandingLinks } from '../../overview/links'; +import { dashboardsLandingLinks, threatHuntingLandingLinks } from '../../landing_pages/links'; +import { gettingStartedLinks } from '../../overview/links'; -export const appLinks: Readonly = Object.freeze([ - gettingStartedLinks, +export const appLinks: AppLinkItems = Object.freeze([ dashboardsLandingLinks, detectionLinks, - { - id: SecurityPageName.threatHuntingLanding, - title: THREAT_HUNTING, - path: THREAT_HUNTING_PATH, - globalNavEnabled: false, - features: [FEATURE.general], - globalSearchKeywords: [ - i18n.translate('xpack.securitySolution.appLinks.threatHunting', { - defaultMessage: 'Threat hunting', - }), - ], - links: [hostsLinks, networkLinks, usersLinks], - }, timelinesLinks, getCasesLinkItems(), + threatHuntingLandingLinks, + gettingStartedLinks, managementLinks, ]); - -export const getAppLinks = async ({ - enableExperimental, - license, - capabilities, -}: UserPermissions) => { - // OLM team, implement async behavior here - return appLinks; -}; diff --git a/x-pack/plugins/security_solution/public/common/links/links.ts b/x-pack/plugins/security_solution/public/common/links/links.ts index 290a1f3fbd820..318f7375078ac 100644 --- a/x-pack/plugins/security_solution/public/common/links/links.ts +++ b/x-pack/plugins/security_solution/public/common/links/links.ts @@ -5,130 +5,100 @@ * 2.0. */ -import { AppDeepLink, AppNavLinkStatus, Capabilities } from '@kbn/core/public'; +import { Capabilities } from '@kbn/core/public'; import { get } from 'lodash'; +import { BehaviorSubject } from 'rxjs'; import { SecurityPageName } from '../../../common/constants'; -import { appLinks, getAppLinks } from './app_links'; +import { appLinks } from './app_links'; import { - Feature, + AppLinkItems, LinkInfo, LinkItem, - NavLinkItem, NormalizedLink, NormalizedLinks, - UserPermissions, + LinksPermissions, } from './types'; -const createDeepLink = (link: LinkItem, linkProps?: UserPermissions): AppDeepLink => ({ - id: link.id, - path: link.path, - title: link.title, - ...(link.links && link.links.length - ? { - deepLinks: reduceLinks({ - links: link.links, - linkProps, - formatFunction: createDeepLink, - }), - } - : {}), - ...(link.icon != null ? { euiIconType: link.icon } : {}), - ...(link.image != null ? { icon: link.image } : {}), - ...(link.globalSearchKeywords != null ? { keywords: link.globalSearchKeywords } : {}), - ...(link.globalNavEnabled != null - ? { navLinkStatus: link.globalNavEnabled ? AppNavLinkStatus.visible : AppNavLinkStatus.hidden } - : {}), - ...(link.globalNavOrder != null ? { order: link.globalNavOrder } : {}), - ...(link.globalSearchEnabled != null ? { searchable: link.globalSearchEnabled } : {}), -}); +export const appLinksUpdater$ = new BehaviorSubject(appLinks); -const createNavLinkItem = (link: LinkItem, linkProps?: UserPermissions): NavLinkItem => ({ - id: link.id, - path: link.path, - title: link.title, - ...(link.description != null ? { description: link.description } : {}), - ...(link.icon != null ? { icon: link.icon } : {}), - ...(link.image != null ? { image: link.image } : {}), - ...(link.links && link.links.length - ? { - links: reduceLinks({ - links: link.links, - linkProps, - formatFunction: createNavLinkItem, - }), - } - : {}), - ...(link.skipUrlState != null ? { skipUrlState: link.skipUrlState } : {}), -}); +export const getAllAppLinks = () => appLinks; -const hasFeaturesCapability = ( - features: Feature[] | undefined, - capabilities: Capabilities -): boolean => { - if (!features) { - return true; - } - return features.some((featureKey) => get(capabilities, featureKey, false)); +export const updateAppLinks = (appLinksToUpdate: LinkItem[]) => { + appLinksUpdater$.next(Object.freeze(appLinksToUpdate)); }; -const isLinkAllowed = (link: LinkItem, linkProps?: UserPermissions) => - !( - linkProps != null && - // exclude link when license is basic and link is premium - ((linkProps.license && !linkProps.license.isAtLeast(link.licenseType ?? 'basic')) || - // exclude link when enableExperimental[hideWhenExperimentalKey] is enabled and link has hideWhenExperimentalKey - (link.hideWhenExperimentalKey != null && - linkProps.enableExperimental[link.hideWhenExperimentalKey]) || - // exclude link when enableExperimental[experimentalKey] is disabled and link has experimentalKey - (link.experimentalKey != null && !linkProps.enableExperimental[link.experimentalKey]) || - // exclude link when link is not part of enabled feature capabilities - (linkProps.capabilities != null && - !hasFeaturesCapability(link.features, linkProps.capabilities))) - ); +export const updateFilteredAppLinks = (linksPermissions: LinksPermissions) => { + updateAppLinks(getFilteredAppLinks(appLinks, linksPermissions)); +}; -export function reduceLinks({ - links, - linkProps, - formatFunction, -}: { - links: Readonly; - linkProps?: UserPermissions; - formatFunction: (link: LinkItem, linkProps?: UserPermissions) => T; -}): T[] { - return links.reduce( - (deepLinks: T[], link: LinkItem) => - isLinkAllowed(link, linkProps) ? [...deepLinks, formatFunction(link, linkProps)] : deepLinks, - [] - ); -} +const getFilteredAppLinks = ( + appLinkToFilter: AppLinkItems, + linksPermissions: LinksPermissions +): LinkItem[] => + appLinkToFilter.reduce((acc, { links, ...appLink }) => { + if (!isLinkAllowed(appLink, linksPermissions)) { + return acc; + } + if (links) { + const childrenLinks = getFilteredAppLinks(links, linksPermissions); + if (childrenLinks.length > 0) { + acc.push({ ...appLink, links: childrenLinks }); + } + } else { + acc.push(appLink); + } + return acc; + }, []); -export const getInitialDeepLinks = (): AppDeepLink[] => { - return appLinks.map((link) => createDeepLink(link)); -}; +// const createNavLinkItem = (link: LinkItem, linkProps?: UserPermissions): NavLinkItem => ({ +// id: link.id, +// path: link.path, +// title: link.title, +// ...(link.description != null ? { description: link.description } : {}), +// ...(link.icon != null ? { icon: link.icon } : {}), +// ...(link.image != null ? { image: link.image } : {}), +// ...(link.links && link.links.length +// ? { +// links: reduceLinks({ +// links: link.links, +// linkProps, +// formatFunction: createNavLinkItem, +// }), +// } +// : {}), +// ...(link.skipUrlState != null ? { skipUrlState: link.skipUrlState } : {}), +// }); -export const getDeepLinks = async ({ - enableExperimental, - license, - capabilities, -}: UserPermissions): Promise => { - const links = await getAppLinks({ enableExperimental, license, capabilities }); - return reduceLinks({ - links, - linkProps: { enableExperimental, license, capabilities }, - formatFunction: createDeepLink, - }); -}; +// It checks if the user has at least one of the link capabilities needed +const hasCapabilities = (linkCapabilities: string[], userCapabilities: Capabilities): boolean => + linkCapabilities.some((linkCapability) => get(userCapabilities, linkCapability, false)); -export const getNavLinkItems = ({ - enableExperimental, - license, - capabilities, -}: UserPermissions): NavLinkItem[] => - reduceLinks({ - links: appLinks, - linkProps: { enableExperimental, license, capabilities }, - formatFunction: createNavLinkItem, - }); +const isLinkAllowed = ( + link: LinkItem, + { license, experimentalFeatures, capabilities, uiSettings }: LinksPermissions +) => { + const linkLicenseType = link.licenseType ?? 'basic'; + if (license) { + if (!license.hasAtLeast(linkLicenseType)) { + return false; + } + } else if (linkLicenseType !== 'basic') { + return false; + } + if (link.hideWhenExperimentalKey && experimentalFeatures[link.hideWhenExperimentalKey]) { + return false; + } + if (link.experimentalKey && !experimentalFeatures[link.experimentalKey]) { + return false; + } + if (link.capabilities && !hasCapabilities(link.capabilities, capabilities)) { + return false; + } + if (link.uiSettingsEnabled && !link.uiSettingsEnabled(uiSettings)) { + return false; + } + return true; +}; /** * Recursive function to create the `NormalizedLinks` structure from a `LinkItem` array parameter diff --git a/x-pack/plugins/security_solution/public/common/links/types.ts b/x-pack/plugins/security_solution/public/common/links/types.ts index eea348b3df737..c87f2d71a79bc 100644 --- a/x-pack/plugins/security_solution/public/common/links/types.ts +++ b/x-pack/plugins/security_solution/public/common/links/types.ts @@ -5,24 +5,17 @@ * 2.0. */ -import { Capabilities } from '@kbn/core/types'; -import { LicenseType } from '@kbn/licensing-plugin/common/types'; -import { LicenseService } from '../../../common/license'; +import { Capabilities, PublicUiSettingsParams, UserProvidedValues } from '@kbn/core/types'; +import { ILicense, LicenseType } from '@kbn/licensing-plugin/common/types'; import { ExperimentalFeatures } from '../../../common/experimental_features'; -import { CASES_FEATURE_ID, SecurityPageName, SERVER_APP_ID } from '../../../common/constants'; +import { SecurityPageName } from '../../../common/constants'; -export const FEATURE = { - general: `${SERVER_APP_ID}.show`, - casesRead: `${CASES_FEATURE_ID}.read_cases`, - casesCrud: `${CASES_FEATURE_ID}.crud_cases`, -}; - -export type Feature = Readonly; - -export interface UserPermissions { - enableExperimental: ExperimentalFeatures; - license?: LicenseService; - capabilities?: Capabilities; +type UiSettings = Readonly>; +export interface LinksPermissions { + experimentalFeatures: Readonly; + capabilities: Capabilities; + uiSettings: UiSettings; + license?: ILicense; } export interface LinkItem { @@ -32,7 +25,7 @@ export interface LinkItem { * Displays deep link when feature flag is enabled. */ experimentalKey?: keyof ExperimentalFeatures; - features?: Feature[]; + capabilities?: string[]; /** * Hides deep link when feature flag is enabled. */ @@ -50,8 +43,11 @@ export interface LinkItem { path: string; skipUrlState?: boolean; // defaults to false title: string; + uiSettingsEnabled?: (uiSettings: UiSettings) => boolean; } +export type AppLinkItems = Readonly; + export interface NavLinkItem { description?: string; icon?: string; diff --git a/x-pack/plugins/security_solution/public/detections/links.ts b/x-pack/plugins/security_solution/public/detections/links.ts index 1cfac62d80e6e..05695836a2035 100644 --- a/x-pack/plugins/security_solution/public/detections/links.ts +++ b/x-pack/plugins/security_solution/public/detections/links.ts @@ -5,15 +5,15 @@ * 2.0. */ import { i18n } from '@kbn/i18n'; -import { ALERTS_PATH, SecurityPageName } from '../../common/constants'; +import { ALERTS_PATH, SecurityPageName, SERVER_APP_ID } from '../../common/constants'; import { ALERTS } from '../app/translations'; -import { LinkItem, FEATURE } from '../common/links/types'; +import { LinkItem } from '../common/links/types'; export const links: LinkItem = { id: SecurityPageName.alerts, title: ALERTS, path: ALERTS_PATH, - features: [FEATURE.general], + capabilities: [`${SERVER_APP_ID}.show`], globalNavEnabled: true, globalSearchKeywords: [ i18n.translate('xpack.securitySolution.appLinks.alerts', { diff --git a/x-pack/plugins/security_solution/public/landing_pages/links.ts b/x-pack/plugins/security_solution/public/landing_pages/links.ts new file mode 100644 index 0000000000000..8409f8d3f5505 --- /dev/null +++ b/x-pack/plugins/security_solution/public/landing_pages/links.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { + DASHBOARDS_PATH, + SecurityPageName, + SERVER_APP_ID, + THREAT_HUNTING_PATH, +} from '../../common/constants'; +import { DASHBOARDS, THREAT_HUNTING } from '../app/translations'; +import { LinkItem } from '../common/links/types'; +import { overviewLinks, detectionResponseLinks } from '../overview/links'; +import { links as hostsLinks } from '../hosts/links'; +import { links as networkLinks } from '../network/links'; +import { links as usersLinks } from '../users/links'; + +export const dashboardsLandingLinks: LinkItem = { + id: SecurityPageName.dashboardsLanding, + title: DASHBOARDS, + path: DASHBOARDS_PATH, + globalNavEnabled: false, + capabilities: [`${SERVER_APP_ID}.show`], + globalSearchKeywords: [ + i18n.translate('xpack.securitySolution.appLinks.dashboards', { + defaultMessage: 'Dashboards', + }), + ], + links: [overviewLinks, detectionResponseLinks], +}; + +export const threatHuntingLandingLinks: LinkItem = { + id: SecurityPageName.threatHuntingLanding, + title: THREAT_HUNTING, + path: THREAT_HUNTING_PATH, + globalNavEnabled: false, + capabilities: [`${SERVER_APP_ID}.show`], + globalSearchKeywords: [ + i18n.translate('xpack.securitySolution.appLinks.threatHunting', { + defaultMessage: 'Threat hunting', + }), + ], + links: [hostsLinks, networkLinks, usersLinks], +}; diff --git a/x-pack/plugins/security_solution/public/management/links.ts b/x-pack/plugins/security_solution/public/management/links.ts index d941d538c80f7..4bb44c2edc610 100644 --- a/x-pack/plugins/security_solution/public/management/links.ts +++ b/x-pack/plugins/security_solution/public/management/links.ts @@ -16,6 +16,7 @@ import { POLICIES_PATH, RULES_PATH, SecurityPageName, + SERVER_APP_ID, TRUSTED_APPS_PATH, } from '../../common/constants'; import { @@ -29,7 +30,7 @@ import { RULES, TRUSTED_APPLICATIONS, } from '../app/translations'; -import { FEATURE, LinkItem } from '../common/links/types'; +import { LinkItem } from '../common/links/types'; export const links: LinkItem = { id: SecurityPageName.administration, @@ -37,7 +38,7 @@ export const links: LinkItem = { path: MANAGEMENT_PATH, skipUrlState: true, globalNavEnabled: false, - features: [FEATURE.general], + capabilities: [`${SERVER_APP_ID}.show`], globalSearchKeywords: [ i18n.translate('xpack.securitySolution.appLinks.manage', { defaultMessage: 'Manage', diff --git a/x-pack/plugins/security_solution/public/overview/links.ts b/x-pack/plugins/security_solution/public/overview/links.ts index 89f75053b3d6f..0dbdc19c1205f 100644 --- a/x-pack/plugins/security_solution/public/overview/links.ts +++ b/x-pack/plugins/security_solution/public/overview/links.ts @@ -7,21 +7,21 @@ import { i18n } from '@kbn/i18n'; import { - DASHBOARDS_PATH, DETECTION_RESPONSE_PATH, LANDING_PATH, OVERVIEW_PATH, SecurityPageName, + SERVER_APP_ID, } from '../../common/constants'; -import { DASHBOARDS, DETECTION_RESPONSE, GETTING_STARTED, OVERVIEW } from '../app/translations'; -import { FEATURE, LinkItem } from '../common/links/types'; +import { DETECTION_RESPONSE, GETTING_STARTED, OVERVIEW } from '../app/translations'; +import { LinkItem } from '../common/links/types'; export const overviewLinks: LinkItem = { id: SecurityPageName.overview, title: OVERVIEW, path: OVERVIEW_PATH, globalNavEnabled: true, - features: [FEATURE.general], + capabilities: [`${SERVER_APP_ID}.show`], globalSearchKeywords: [ i18n.translate('xpack.securitySolution.appLinks.overview', { defaultMessage: 'Overview', @@ -35,7 +35,7 @@ export const gettingStartedLinks: LinkItem = { title: GETTING_STARTED, path: LANDING_PATH, globalNavEnabled: false, - features: [FEATURE.general], + capabilities: [`${SERVER_APP_ID}.show`], globalSearchKeywords: [ i18n.translate('xpack.securitySolution.appLinks.getStarted', { defaultMessage: 'Getting started', @@ -50,24 +50,10 @@ export const detectionResponseLinks: LinkItem = { path: DETECTION_RESPONSE_PATH, globalNavEnabled: false, experimentalKey: 'detectionResponseEnabled', - features: [FEATURE.general], + capabilities: [`${SERVER_APP_ID}.show`], globalSearchKeywords: [ i18n.translate('xpack.securitySolution.appLinks.detectionAndResponse', { defaultMessage: 'Detection & Response', }), ], }; - -export const dashboardsLandingLinks: LinkItem = { - id: SecurityPageName.dashboardsLanding, - title: DASHBOARDS, - path: DASHBOARDS_PATH, - globalNavEnabled: false, - features: [FEATURE.general], - globalSearchKeywords: [ - i18n.translate('xpack.securitySolution.appLinks.dashboards', { - defaultMessage: 'Dashboards', - }), - ], - links: [overviewLinks, detectionResponseLinks], -}; diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 4b49c04f295a5..e44a3b935e765 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -47,7 +47,7 @@ import { SOURCERER_API_URL, } from '../common/constants'; -import { getDeepLinks } from './app/deep_links'; +import { getAllDeepLinks, registerDeepLinksUpdater } from './app/deep_links'; import { getSubPluginRoutesByCapabilities, manageOldSiemRoutes } from './helpers'; import { SecurityAppStore } from './common/store/store'; import { licenseService } from './common/hooks/use_license'; @@ -64,6 +64,8 @@ import { import { LazyEndpointCustomAssetsExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_custom_assets_extension'; import { initDataView, SourcererModel, KibanaDataView } from './common/store/sourcerer/model'; import { SecurityDataView } from './common/containers/sourcerer/api'; +import { updateFilteredAppLinks } from './common/links'; +import { LinksPermissions } from './common/links/types'; export class Plugin implements IPlugin { readonly kibanaVersion: string; @@ -140,7 +142,7 @@ export class Plugin implements IPlugin { // required to show the alert table inside cases const { alertsTableConfigurationRegistry } = plugins.triggersActionsUi; @@ -222,32 +224,29 @@ export class Plugin implements IPlugin { if (currentLicense.type !== undefined) { - this.appUpdater$.next(() => ({ - navLinkStatus: AppNavLinkStatus.hidden, // workaround to prevent main navLink to switch to visible after update. should not be needed - deepLinks: getDeepLinks( - this.experimentalFeatures, - currentLicense.type, - core.application.capabilities - ), - })); + updateFilteredAppLinks({ ...linksPermissions, license: currentLicense }); + } else { + updateFilteredAppLinks(linksPermissions); } }); } else { - this.appUpdater$.next(() => ({ - navLinkStatus: AppNavLinkStatus.hidden, // workaround to prevent main navLink to switch to visible after update. should not be needed - deepLinks: getDeepLinks( - this.experimentalFeatures, - undefined, - core.application.capabilities - ), - })); + updateFilteredAppLinks(linksPermissions); } return {}; diff --git a/x-pack/plugins/security_solution/public/timelines/links.ts b/x-pack/plugins/security_solution/public/timelines/links.ts index 1bdadb20cfa6d..b7cf4a818e111 100644 --- a/x-pack/plugins/security_solution/public/timelines/links.ts +++ b/x-pack/plugins/security_solution/public/timelines/links.ts @@ -6,16 +6,16 @@ */ import { i18n } from '@kbn/i18n'; -import { SecurityPageName, TIMELINES_PATH } from '../../common/constants'; +import { SecurityPageName, SERVER_APP_ID, TIMELINES_PATH } from '../../common/constants'; import { TIMELINES } from '../app/translations'; -import { FEATURE, LinkItem } from '../common/links/types'; +import { LinkItem } from '../common/links/types'; export const links: LinkItem = { id: SecurityPageName.timelines, title: TIMELINES, path: TIMELINES_PATH, globalNavEnabled: true, - features: [FEATURE.general], + capabilities: [`${SERVER_APP_ID}.show`], globalSearchKeywords: [ i18n.translate('xpack.securitySolution.appLinks.timelines', { defaultMessage: 'Timelines', From b18c4c447f99d407ac0982480af7d670ebab414b Mon Sep 17 00:00:00 2001 From: semd Date: Mon, 16 May 2022 10:05:47 +0200 Subject: [PATCH 04/25] navigation panel categories --- .../solution_nav/solution_nav.scss | 3 + .../public/app/deep_links/index.ts | 9 +- .../app/home/template_wrapper/index.tsx | 26 +- .../security_solution/public/cases/links.ts | 2 + .../common/components/navigation/nav_links.ts | 33 ++- .../security_side_nav/icons/launch.tsx | 33 +++ .../navigation/security_side_nav/index.ts | 8 + .../security_side_nav/security_side_nav.tsx | 118 +++++++++ .../solution_grouped_nav.tsx | 248 +++++++++++------- .../solution_grouped_nav_item.tsx | 188 ------------- .../solution_grouped_nav_panel.tsx | 126 +++++++-- .../navigation/solution_grouped_nav/types.ts | 32 +++ .../common/components/navigation/types.ts | 11 + .../use_primary_navigation.tsx | 6 +- .../public/common/links/app_links.ts | 4 +- .../public/common/links/index.tsx | 2 + .../public/common/links/links.ts | 60 +++-- .../public/common/links/types.ts | 72 +++-- .../public/landing_pages/pages/manage.tsx | 2 +- .../public/management/links.ts | 4 +- .../security_solution/public/plugin.tsx | 8 +- .../public/timelines/links.ts | 1 + 22 files changed, 605 insertions(+), 391 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/icons/launch.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/index.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_item.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/types.ts diff --git a/packages/kbn-shared-ux-components/src/page_template/solution_nav/solution_nav.scss b/packages/kbn-shared-ux-components/src/page_template/solution_nav/solution_nav.scss index 91b96641047e8..3c520828de3ea 100644 --- a/packages/kbn-shared-ux-components/src/page_template/solution_nav/solution_nav.scss +++ b/packages/kbn-shared-ux-components/src/page_template/solution_nav/solution_nav.scss @@ -18,6 +18,9 @@ $euiSideNavEmphasizedBackgroundColor: transparentize($euiColorLightShade, .7); .kbnPageTemplateSolutionNav__avatar { margin-right: $euiSize; } + + display: flex; + flex-direction: column; } .kbnPageTemplateSolutionNav--hidden { diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.ts index ff9355edd4d86..591c020899021 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.ts @@ -62,8 +62,9 @@ import { MANAGE_PATH, } from '../../../common/constants'; import { ExperimentalFeatures } from '../../../common/experimental_features'; -import { getAllAppLinks, appLinksUpdater$ } from '../../common/links'; -import { LinkItem } from '../../common/links/types'; +import { subscribeAppLinks } from '../../common/links'; +import { getAllAppLinks } from '../../common/links/app_links'; +import { AppLinkItems } from '../../common/links/types'; const FEATURE = { general: `${SERVER_APP_ID}.show`, @@ -555,7 +556,7 @@ export function isPremiumLicense(licenseType?: LicenseType): boolean { // Returns all deep links without filtering, for the initial application register export const getAllDeepLinks = (): AppDeepLink[] => formatDeepLinks(getAllAppLinks()); -const formatDeepLinks = (appLinks: readonly LinkItem[]): AppDeepLink[] => +const formatDeepLinks = (appLinks: AppLinkItems): AppDeepLink[] => appLinks.map((appLink) => ({ id: appLink.id, path: appLink.path, @@ -572,7 +573,7 @@ const formatDeepLinks = (appLinks: readonly LinkItem[]): AppDeepLink[] => })); export const registerDeepLinksUpdater = (appUpdater$: Subject) => { - appLinksUpdater$.subscribe((appLinks) => { + subscribeAppLinks((appLinks) => { appUpdater$.next(() => ({ navLinkStatus: AppNavLinkStatus.hidden, // needed to prevent main security link to switch to visible after update deepLinks: formatDeepLinks(appLinks), diff --git a/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx b/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx index 3b436d2bdefc1..3425485582ecf 100644 --- a/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx @@ -26,6 +26,7 @@ import { gutterTimeline } from '../../../common/lib/helpers'; import { useKibana } from '../../../common/lib/kibana'; import { useShowPagesWithEmptyView } from '../../../common/utils/empty_view/use_show_pages_with_empty_view'; import { useIsPolicySettingsBarVisible } from '../../../management/pages/policy/view/policy_hooks'; +import { useIsGroupedNavigationEnabled } from '../../../common/components/navigation/helpers'; const NO_DATA_PAGE_MAX_WIDTH = 950; @@ -44,8 +45,8 @@ const NO_DATA_PAGE_TEMPLATE_PROPS = { */ const StyledKibanaPageTemplate = styled(KibanaPageTemplate)<{ $isShowingTimelineOverlay?: boolean; - $isTimelineBottomBarVisible?: boolean; - $isPolicySettingsVisible?: boolean; + $isBottomBarVisible?: boolean; + $isGroupedNav?: boolean; }>` .${BOTTOM_BAR_CLASSNAME} { animation: 'none !important'; // disable the default bottom bar slide animation @@ -63,8 +64,8 @@ const StyledKibanaPageTemplate = styled(KibanaPageTemplate)<{ } // If the bottom bar is visible add padding to the navigation - ${({ $isTimelineBottomBarVisible }) => - $isTimelineBottomBarVisible && + ${({ $isBottomBarVisible, $isGroupedNav }) => + ($isBottomBarVisible || $isGroupedNav) && ` @media (min-width: 768px) { .kbnPageTemplateSolutionNav { @@ -73,14 +74,12 @@ const StyledKibanaPageTemplate = styled(KibanaPageTemplate)<{ } `} - // If the policy settings bottom bar is visible add padding to the navigation - ${({ $isPolicySettingsVisible }) => - $isPolicySettingsVisible && + ${({ $isGroupedNav }) => + $isGroupedNav && ` - @media (min-width: 768px) { - .kbnPageTemplateSolutionNav { - padding-bottom: ${gutterTimeline}; - } + .kbnPageTemplateSolutionNav { + display: flex; + flex-direction: column; } `} `; @@ -98,6 +97,7 @@ export const SecuritySolutionTemplateWrapper: React.FC getTimelineShowStatus(state, TimelineId.active) ); + const isGroupedNavEnabled = useIsGroupedNavigationEnabled(); const userHasSecuritySolutionVisible = useKibana().services.application.capabilities.siem.show; const showEmptyState = useShowPagesWithEmptyView(); @@ -117,9 +117,9 @@ export const SecuritySolutionTemplateWrapper: React.FC diff --git a/x-pack/plugins/security_solution/public/cases/links.ts b/x-pack/plugins/security_solution/public/cases/links.ts index 3484454a4f02b..fd6a991687425 100644 --- a/x-pack/plugins/security_solution/public/cases/links.ts +++ b/x-pack/plugins/security_solution/public/cases/links.ts @@ -21,9 +21,11 @@ export const getCasesLinkItems = (): LinkItem => { [SecurityPageName.caseConfigure]: { capabilities: [`${CASES_FEATURE_ID}.crud_cases`], licenseType: 'gold', + sideNavDisabled: true, }, [SecurityPageName.caseCreate]: { capabilities: [`${CASES_FEATURE_ID}.crud_cases`], + sideNavDisabled: true, }, }, }); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/nav_links.ts b/x-pack/plugins/security_solution/public/common/components/navigation/nav_links.ts index efdf72a1f7926..6d45d0aeed764 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/nav_links.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/nav_links.ts @@ -5,21 +5,34 @@ * 2.0. */ -import { useKibana } from '../../lib/kibana'; -import { useEnableExperimental } from '../../hooks/use_experimental_features'; -import { useLicense } from '../../hooks/use_license'; -import { getNavLinkItems } from '../../links'; +import { useMemo } from 'react'; +import { useAppLinks } from '../../links'; import type { SecurityPageName } from '../../../app/types'; -import type { NavLinkItem } from '../../links/types'; +import { NavLinkItem } from './types'; +import { AppLinkItems } from '../../links/types'; export const useAppNavLinks = (): NavLinkItem[] => { - const license = useLicense(); - const enableExperimental = useEnableExperimental(); - const capabilities = useKibana().services.application.capabilities; - - return getNavLinkItems({ enableExperimental, license, capabilities }); + const appLinks = useAppLinks(); + const navLinks = useMemo(() => formatNavLinkItems(appLinks), [appLinks]); + return navLinks; }; export const useAppRootNavLink = (linkId: SecurityPageName): NavLinkItem | undefined => { return useAppNavLinks().find(({ id }) => id === linkId); }; + +const formatNavLinkItems = (appLinks: AppLinkItems): NavLinkItem[] => + appLinks.map((link) => ({ + id: link.id, + title: link.title, + ...(link.description != null ? { description: link.description } : {}), + ...(link.sideNavDisabled === true ? { disabled: true } : {}), + ...(link.landingIcon != null ? { icon: link.landingIcon } : {}), + ...(link.landingImage != null ? { image: link.landingImage } : {}), + ...(link.skipUrlState != null ? { skipUrlState: link.skipUrlState } : {}), + ...(link.links && link.links.length + ? { + links: formatNavLinkItems(link.links), + } + : {}), + })); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/icons/launch.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/icons/launch.tsx new file mode 100644 index 0000000000000..90742327a7d04 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/icons/launch.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { SVGProps } from 'react'; + +export const EuiIconLaunch: React.FC> = ({ ...props }) => ( + + + + + + + +); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/index.ts b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/index.ts new file mode 100644 index 0000000000000..a2c866e604e16 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { SecuritySideNav } from './security_side_nav'; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.tsx new file mode 100644 index 0000000000000..41525f81d5207 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.tsx @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo, useCallback } from 'react'; +import { EuiLink, EuiListGroupItem } from '@elastic/eui'; +import { SecurityPageName } from '../../../../app/types'; +import { navigationCategories as managementCategories } from '../../../../management/links'; +import { getAncestorLinksInfo } from '../../../links'; +import { useRouteSpy } from '../../../utils/route/use_route_spy'; +import { useGetSecuritySolutionLinkProps } from '../../links'; +import { useAppNavLinks } from '../nav_links'; +import { SolutionGroupedNav } from '../solution_grouped_nav'; +import { CustomSideNavItem, DefaultSideNavItem, SideNavItem } from '../solution_grouped_nav/types'; +import { NavLinkItem } from '../types'; +import { EuiIconLaunch } from './icons/launch'; + +const isFooterNavItem = (id: SecurityPageName) => + id === SecurityPageName.landing || id === SecurityPageName.administration; + +type FormatSideNavItems = (navItems: NavLinkItem) => SideNavItem; + +const useFormatSideNavItem = (): FormatSideNavItems => { + const getSecuritySolutionLinkProps = useGetSecuritySolutionLinkProps(); // adds href and onClick props + + const formatSideNavItem: FormatSideNavItems = useCallback( + (navLinkItem) => { + const formatDefaultItem = (navItem: NavLinkItem): DefaultSideNavItem => ({ + id: navItem.id, + label: navItem.title, + ...getSecuritySolutionLinkProps({ deepLinkId: navItem.id }), + ...(navItem.links && navItem.links.length > 0 + ? { + items: navItem.links + .filter((link) => !link.disabled) + .map((panelNavItem) => ({ + id: panelNavItem.id, + label: panelNavItem.title, + description: panelNavItem.description, + ...getSecuritySolutionLinkProps({ deepLinkId: panelNavItem.id }), + })), + } + : {}), + }); + + const formatManagementItem = (navItem: NavLinkItem): DefaultSideNavItem => ({ + ...formatDefaultItem(navItem), + categories: managementCategories, + }); + + const formatGetStartedItem = (navItem: NavLinkItem): CustomSideNavItem => ({ + id: navItem.id, + render: () => ( + + + + ), + }); + + if (navLinkItem.id === SecurityPageName.administration) { + return formatManagementItem(navLinkItem); + } + if (navLinkItem.id === SecurityPageName.landing) { + return formatGetStartedItem(navLinkItem); + } + return formatDefaultItem(navLinkItem); + }, + [getSecuritySolutionLinkProps] + ); + + return formatSideNavItem; +}; + +const useSideNavItems = () => { + const appNavLinks = useAppNavLinks(); + const formatSideNavItem = useFormatSideNavItem(); + + const sideNavItems = useMemo(() => { + const mainNavItems: SideNavItem[] = []; + const footerNavItems: SideNavItem[] = []; + appNavLinks.forEach((appNavLink) => { + if (appNavLink.disabled) { + return; + } + if (isFooterNavItem(appNavLink.id)) { + footerNavItems.push(formatSideNavItem(appNavLink)); + } else { + mainNavItems.push(formatSideNavItem(appNavLink)); + } + }); + return [mainNavItems, footerNavItems]; + }, [appNavLinks, formatSideNavItem]); + + return sideNavItems; +}; + +const useSelectedId = (): SecurityPageName => { + const [{ pageName }] = useRouteSpy(); + + const selectedId = useMemo(() => { + const [rootLinkInfo] = getAncestorLinksInfo(pageName as SecurityPageName); + return rootLinkInfo?.id ?? ''; + }, [pageName]); + + return selectedId; +}; + +const SecuritySideNavComponent: React.FC = () => { + const [items, footerItems] = useSideNavItems(); + const selectedId = useSelectedId(); + return ; +}; +SecuritySideNavComponent.displayName = 'SecuritySideNavComponent'; + +export const SecuritySideNav = SecuritySideNavComponent; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.tsx index fcfcc9d6b1b4b..b1a954ca822ba 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.tsx @@ -15,22 +15,38 @@ import { } from '@elastic/eui'; import classNames from 'classnames'; -import { SolutionGroupedNavPanel } from './solution_grouped_nav_panel'; +import { SolutionNavPanel } from './solution_grouped_nav_panel'; import { EuiListGroupItemStyled } from './solution_grouped_nav.styles'; -import { - isCustomNavItem, - isDefaultNavItem, - NavItem, - PortalNavItem, -} from './solution_grouped_nav_item'; +import { DefaultSideNavItem, SideNavItem, isCustomItem, isDefaultItem } from './types'; import { EuiIconSpaces } from './icons/spaces'; +import { NavigationCategories } from '../types'; export interface SolutionGroupedNavProps { - items: NavItem[]; + items: SideNavItem[]; + selectedId: string; + footerItems?: SideNavItem[]; +} +export interface SolutionNavItemsProps { + items: SideNavItem[]; selectedId: string; - footerItems?: NavItem[]; + activePanelNavId: ActivePanelNav; + isMobileSize: boolean; + navItemsById: NavItemsById; + onOpenPanelNav: (id: string) => void; } -type ActivePortalNav = string | null; +export interface SolutionNavItemProps { + item: SideNavItem; + isSelected: boolean; + isActive: boolean; + hasPanelNav: boolean; + onOpenPanelNav: (id: string) => void; +} + +type ActivePanelNav = string | null; +type NavItemsById = Record< + string, + { title: string; panelItems: DefaultSideNavItem[]; categories?: NavigationCategories } +>; export const SolutionGroupedNavComponent: React.FC = ({ items, @@ -39,41 +55,40 @@ export const SolutionGroupedNavComponent: React.FC = ({ }) => { const isMobileSize = useIsWithinBreakpoints(['xs', 's']); - const [activePortalNavId, setActivePortalNavId] = useState(null); - const activePortalNavIdRef = useRef(null); + const [activePanelNavId, setActivePanelNavId] = useState(null); + const activePanelNavIdRef = useRef(null); - const openPortalNav = (navId: string) => { - activePortalNavIdRef.current = navId; - setActivePortalNavId(navId); + const openPanelNav = (id: string) => { + activePanelNavIdRef.current = id; + setActivePanelNavId(id); }; - const closePortalNav = () => { - activePortalNavIdRef.current = null; - setActivePortalNavId(null); + const closePanelNav = () => { + activePanelNavIdRef.current = null; + setActivePanelNavId(null); }; - const onClosePortalNav = useCallback(() => { - const currentPortalNavId = activePortalNavIdRef.current; + const onClosePanelNav = useCallback(() => { + const currentPanelNavId = activePanelNavIdRef.current; setTimeout(() => { // This event is triggered on outside click. // Closing the side nav at the end of event loop to make sure it - // closes also if the active "nav group" button has been clicked (toggle), - // but it does not close if any some other "nav group" open button has been clicked. - if (activePortalNavIdRef.current === currentPortalNavId) { - closePortalNav(); + // closes also if the active panel button has been clicked (toggle), + // but it does not close if any some other panel open button has been clicked. + if (activePanelNavIdRef.current === currentPanelNavId) { + closePanelNav(); } }); }, []); - const navItemsById = useMemo( + const navItemsById = useMemo( () => - [...items, ...footerItems].reduce< - Record - >((acc, navItem) => { - if (isDefaultNavItem(navItem) && navItem.items && navItem.items.length > 0) { + [...items, ...footerItems].reduce((acc, navItem) => { + if (isDefaultItem(navItem) && navItem.items && navItem.items.length > 0) { acc[navItem.id] = { title: navItem.label, - subItems: navItem.items, + panelItems: navItem.items, + categories: navItem.categories, }; } return acc; @@ -82,67 +97,19 @@ export const SolutionGroupedNavComponent: React.FC = ({ ); const portalNav = useMemo(() => { - if (activePortalNavId == null || !navItemsById[activePortalNavId]) { + if (activePanelNavId == null || !navItemsById[activePanelNavId]) { return null; } - const { subItems, title } = navItemsById[activePortalNavId]; - return ; - }, [activePortalNavId, navItemsById, onClosePortalNav]); - - const renderNavItem = useCallback( - (navItem: NavItem) => { - if (isCustomNavItem(navItem)) { - return {navItem.render()}; - } - const { id, href, label, onClick } = navItem; - const isActive = activePortalNavId === id; - const isCurrentNav = selectedId === id; - - const itemClassNames = classNames('solutionGroupedNavItem', { - 'solutionGroupedNavItem--isActive': isActive, - 'solutionGroupedNavItem--isPrimary': isCurrentNav, - }); - const buttonClassNames = classNames('solutionGroupedNavItemButton'); - - return ( - // eslint-disable-next-line @elastic/eui/href-or-on-click - - { - ev.preventDefault(); - ev.stopPropagation(); - openPortalNav(id); - }, - iconType: EuiIconSpaces, - iconSize: 'm', - 'aria-label': 'Toggle group nav', - 'data-test-subj': `groupedNavItemButton-${id}`, - alwaysShow: true, - }, - } - : {})} - /> - - ); - }, - [activePortalNavId, isMobileSize, navItemsById, selectedId] - ); + const { panelItems, title, categories } = navItemsById[activePanelNavId]; + return ( + + ); + }, [activePanelNavId, navItemsById, onClosePanelNav]); return ( <> @@ -150,10 +117,28 @@ export const SolutionGroupedNavComponent: React.FC = ({ - {items.map(renderNavItem)} + + + - {footerItems.map(renderNavItem)} + + + @@ -163,5 +148,84 @@ export const SolutionGroupedNavComponent: React.FC = ({ ); }; - export const SolutionGroupedNav = React.memo(SolutionGroupedNavComponent); + +const SolutionNavItems: React.FC = ({ + items, + selectedId, + activePanelNavId, + isMobileSize, + navItemsById, + onOpenPanelNav, +}) => ( + <> + {items.map((item) => ( + + ))} + +); + +const SolutionNavItemComponent: React.FC = ({ + item, + isSelected, + isActive, + hasPanelNav, + onOpenPanelNav, +}) => { + if (isCustomItem(item)) { + return {item.render()}; + } + const { id, href, label, onClick } = item; + + const itemClassNames = classNames('solutionGroupedNavItem', { + 'solutionGroupedNavItem--isActive': isActive, + 'solutionGroupedNavItem--isPrimary': isSelected, + }); + const buttonClassNames = classNames('solutionGroupedNavItemButton'); + + const onButtonClick: React.MouseEventHandler = (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + onOpenPanelNav(id); + }; + + return ( + // eslint-disable-next-line @elastic/eui/href-or-on-click + + + + ); +}; +const SolutionNavItem = React.memo(SolutionNavItemComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_item.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_item.tsx deleted file mode 100644 index df7e08ad46f95..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_item.tsx +++ /dev/null @@ -1,188 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { useGetSecuritySolutionLinkProps } from '../../links'; -import { SecurityPageName } from '../../../../../common/constants'; - -export type NavItemCategories = Array<{ label: string; itemIds: string[] }>; -export interface DefaultNavItem { - id: string; - label: string; - href: string; - onClick?: React.MouseEventHandler; - items?: PortalNavItem[]; - categories?: NavItemCategories; -} - -export interface CustomNavItem { - id: string; - render: () => React.ReactNode; -} - -export type NavItem = DefaultNavItem | CustomNavItem; - -export interface PortalNavItem { - id: string; - label: string; - href: string; - onClick?: React.MouseEventHandler; - description?: string; -} - -export const isCustomNavItem = (navItem: NavItem): navItem is CustomNavItem => 'render' in navItem; -export const isDefaultNavItem = (navItem: NavItem): navItem is DefaultNavItem => - !isCustomNavItem(navItem); - -export const useNavItems: () => NavItem[] = () => { - const getSecuritySolutionLinkProps = useGetSecuritySolutionLinkProps(); - return [ - { - id: SecurityPageName.dashboardsLanding, - label: 'Dashboards', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.dashboardsLanding }), - items: [ - { - id: 'overview', - label: 'Overview', - description: 'The description goes here', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.overview }), - }, - { - id: 'detection_response', - label: 'Detection & Response', - description: 'The description goes here', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.detectionAndResponse }), - }, - // TODO: add the cloudPostureFindings to the config here - // { - // id: SecurityPageName.cloudPostureFindings, - // label: 'Cloud Posture Findings', - // description: 'The description goes here', - // ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.cloudPostureFindings }), - // }, - ], - }, - { - id: SecurityPageName.alerts, - label: 'Alerts', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.alerts }), - }, - { - id: SecurityPageName.timelines, - label: 'Timelines', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.timelines }), - }, - { - id: SecurityPageName.case, - label: 'Cases', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.case }), - }, - { - id: SecurityPageName.threatHuntingLanding, - label: 'Threat Hunting', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.threatHuntingLanding }), - items: [ - { - id: SecurityPageName.hosts, - label: 'Hosts', - description: - 'Computer or other device that communicates with other hosts on a network. Hosts on a network include clients and servers -- that send or receive data, services or applications.', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.hosts }), - }, - { - id: SecurityPageName.network, - label: 'Network', - description: - 'The action or process of interacting with others to exchange information and develop professional or social contacts.', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.network }), - }, - { - id: SecurityPageName.users, - label: 'Users', - description: 'Sudo commands dashboard from the Logs System integration.', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.users }), - }, - ], - }, - // TODO: implement footer and move management - { - id: SecurityPageName.administration, - label: 'Manage', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.administration }), - categories: [ - { label: 'SIEM', itemIds: [SecurityPageName.rules, SecurityPageName.exceptions] }, - { - label: 'ENDPOINTS', - itemIds: [ - SecurityPageName.endpoints, - SecurityPageName.policies, - SecurityPageName.trustedApps, - SecurityPageName.eventFilters, - SecurityPageName.blocklist, - SecurityPageName.hostIsolationExceptions, - ], - }, - ], - items: [ - { - id: SecurityPageName.rules, - label: 'Rules', - description: 'The description here', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.rules }), - }, - { - id: SecurityPageName.exceptions, - label: 'Exceptions', - description: 'The description goes here', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.exceptions }), - }, - { - id: SecurityPageName.endpoints, - label: 'Endpoints', - description: 'The description goes here', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.endpoints }), - }, - { - id: SecurityPageName.policies, - label: 'Policies', - description: 'The description goes here', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.policies }), - }, - { - id: SecurityPageName.trustedApps, - label: 'Trusted applications', - description: 'The description goes here', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.trustedApps }), - }, - { - id: SecurityPageName.eventFilters, - label: 'Event filters', - description: 'The description goes here', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.eventFilters }), - }, - { - id: SecurityPageName.blocklist, - label: 'Blocklist', - description: 'The description goes here', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.blocklist }), - }, - { - id: SecurityPageName.hostIsolationExceptions, - label: 'Host Isolation IP exceptions', - description: 'The description goes here', - ...getSecuritySolutionLinkProps({ deepLinkId: SecurityPageName.hostIsolationExceptions }), - }, - ], - }, - ]; -}; - -export const useFooterNavItems: () => NavItem[] = () => { - // TODO: implement footer items - return []; -}; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_panel.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_panel.tsx index c1615a97264eb..dafe18ed8aa53 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_panel.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_panel.tsx @@ -13,8 +13,10 @@ import { EuiFlexGroup, EuiFlexItem, EuiFocusTrap, + EuiHorizontalRule, EuiOutsideClickDetector, EuiPortal, + EuiSpacer, EuiTitle, EuiWindowEvent, keys, @@ -22,18 +24,37 @@ import { } from '@elastic/eui'; import classNames from 'classnames'; import { EuiPanelStyled } from './solution_grouped_nav_panel.styles'; -import { PortalNavItem } from './solution_grouped_nav_item'; import { useShowTimeline } from '../../../utils/timeline/use_show_timeline'; +import type { DefaultSideNavItem } from './types'; +import { NavigationCategories } from '../types'; -export interface SolutionGroupedNavPanelProps { +export interface SolutionNavPanelProps { onClose: () => void; title: string; - items: PortalNavItem[]; + items: DefaultSideNavItem[]; + categories?: NavigationCategories; +} +export interface SolutionNavPanelCategoriesProps { + categories: NavigationCategories; + items: DefaultSideNavItem[]; + onClose: () => void; +} +export interface SolutionNavPanelItemsProps { + items: DefaultSideNavItem[]; + onClose: () => void; +} +export interface SolutionNavPanelItemProps { + item: DefaultSideNavItem; + onClose: () => void; } -const SolutionGroupedNavPanelComponent: React.FC = ({ +/** + * Renders the side navigation panel for secondary links + */ +const SolutionNavPanelComponent: React.FC = ({ onClose, title, + categories, items, }) => { const [hasTimelineBar] = useShowTimeline(); @@ -41,9 +62,7 @@ const SolutionGroupedNavPanelComponent: React.FC = const isTimelineVisible = hasTimelineBar && isLargerBreakpoint; const panelClasses = classNames('eui-yScroll'); - /** - * ESC key closes SideNav - */ + // ESC key closes PanelNav const onKeyDown = useCallback( (ev: KeyboardEvent) => { if (ev.key === keys.ESCAPE) { @@ -76,25 +95,15 @@ const SolutionGroupedNavPanelComponent: React.FC = - {items.map(({ id, href, onClick, label, description }: PortalNavItem) => ( - - - { - onClose(); - if (onClick) { - onClick(ev); - } - }} - > - {label} - - - {description} - - ))} + {categories ? ( + + ) : ( + + )}
@@ -105,5 +114,68 @@ const SolutionGroupedNavPanelComponent: React.FC = ); }; +export const SolutionNavPanel = React.memo(SolutionNavPanelComponent); + +const SolutionNavPanelCategories: React.FC = ({ + categories, + items, + onClose, +}) => { + const itemsById = new Map(items.map((item) => [item.id, item])); + + return ( + <> + {categories.map(({ label, linkIds }) => { + const links = linkIds.reduce((acc, linkId) => { + const link = itemsById.get(linkId); + if (link) { + acc.push(link); + } + return acc; + }, []); + + return ( + + +

{label}

+
+ + + +
+ ); + })} + + ); +}; -export const SolutionGroupedNavPanel = React.memo(SolutionGroupedNavPanelComponent); +const SolutionNavPanelItems: React.FC = ({ items, onClose }) => ( + <> + {items.map((item) => ( + + ))} + +); + +const SolutionNavPanelItem: React.FC = ({ + item: { id, href, onClick, label, description }, + onClose, +}) => ( + <> + + { + onClose(); + if (onClick) { + onClick(ev); + } + }} + > + {label} + + + {description} + +); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/types.ts b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/types.ts new file mode 100644 index 0000000000000..e0b62d6fbd45d --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/types.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { NavigationCategories } from '../types'; +import type { SecurityPageName } from '../../../../app/types'; + +export interface DefaultSideNavItem { + id: SecurityPageName; + label: string; + href: string; + onClick?: React.MouseEventHandler; + description?: string; + items?: DefaultSideNavItem[]; + categories?: NavigationCategories; +} + +export interface CustomSideNavItem { + id: string; + render: () => React.ReactNode; +} + +export type SideNavItem = DefaultSideNavItem | CustomSideNavItem; + +export const isCustomItem = (navItem: SideNavItem): navItem is CustomSideNavItem => + 'render' in navItem; +export const isDefaultItem = (navItem: SideNavItem): navItem is DefaultSideNavItem => + !isCustomItem(navItem); 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 91edd1feea2da..81d4341aea6bc 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 @@ -5,6 +5,7 @@ * 2.0. */ +import { IconType } from '@elastic/eui'; import { UrlStateType } from '../url_state/constants'; import { SecurityPageName } from '../../../app/types'; import { UrlState } from '../url_state/types'; @@ -83,3 +84,13 @@ export interface NavigationCategory { } export type NavigationCategories = Readonly; +export interface NavLinkItem { + description?: string; + icon?: IconType; + id: SecurityPageName; + links?: NavLinkItem[]; + image?: string; + title: string; + skipUrlState?: boolean; + disabled?: boolean; +} diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_primary_navigation.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_primary_navigation.tsx index 1dbcf929ed81f..1123fd50a53e6 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_primary_navigation.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_primary_navigation.tsx @@ -11,9 +11,8 @@ import { i18n } from '@kbn/i18n'; import { KibanaPageTemplateProps } from '@kbn/shared-ux-components'; import { PrimaryNavigationProps } from './types'; import { usePrimaryNavigationItems } from './use_navigation_items'; -import { SolutionGroupedNav } from '../solution_grouped_nav'; -import { useNavItems } from '../solution_grouped_nav/solution_grouped_nav_item'; import { useIsGroupedNavigationEnabled } from '../helpers'; +import { SecuritySideNav } from '../security_side_nav'; const translatedNavTitle = i18n.translate('xpack.securitySolution.navigation.mainLabel', { defaultMessage: 'Security', @@ -48,7 +47,6 @@ export const usePrimaryNavigation = ({ // we do need navTabs in case the selectedTabId appears after initial load (ex. checking permissions for anomalies) }, [pageName, navTabs, mapLocationToTab, selectedTabId]); - const navLinkItems = useNavItems(); const navItems = usePrimaryNavigationItems({ navTabs, selectedTabId, @@ -65,7 +63,7 @@ export const usePrimaryNavigation = ({ icon: 'logoSecurity', ...(isGroupedNavigationEnabled ? { - children: , + children: , closeFlyoutButtonPosition: 'inside', } : { items: navItems }), diff --git a/x-pack/plugins/security_solution/public/common/links/app_links.ts b/x-pack/plugins/security_solution/public/common/links/app_links.ts index a1b134e63b4bc..c1f1a39a495e3 100644 --- a/x-pack/plugins/security_solution/public/common/links/app_links.ts +++ b/x-pack/plugins/security_solution/public/common/links/app_links.ts @@ -12,7 +12,7 @@ import { links as managementLinks } from '../../management/links'; import { dashboardsLandingLinks, threatHuntingLandingLinks } from '../../landing_pages/links'; import { gettingStartedLinks } from '../../overview/links'; -export const appLinks: AppLinkItems = Object.freeze([ +const appLinks: AppLinkItems = Object.freeze([ dashboardsLandingLinks, detectionLinks, timelinesLinks, @@ -21,3 +21,5 @@ export const appLinks: AppLinkItems = Object.freeze([ gettingStartedLinks, managementLinks, ]); + +export const getAllAppLinks = () => appLinks; diff --git a/x-pack/plugins/security_solution/public/common/links/index.tsx b/x-pack/plugins/security_solution/public/common/links/index.tsx index 6d8e99cd416d2..d9565a8edb98d 100644 --- a/x-pack/plugins/security_solution/public/common/links/index.tsx +++ b/x-pack/plugins/security_solution/public/common/links/index.tsx @@ -6,3 +6,5 @@ */ export * from './links'; +export * from './app_links'; +export * from './types'; diff --git a/x-pack/plugins/security_solution/public/common/links/links.ts b/x-pack/plugins/security_solution/public/common/links/links.ts index 318f7375078ac..2f77dbd5ef43a 100644 --- a/x-pack/plugins/security_solution/public/common/links/links.ts +++ b/x-pack/plugins/security_solution/public/common/links/links.ts @@ -7,9 +7,10 @@ import { Capabilities } from '@kbn/core/public'; import { get } from 'lodash'; +import { useEffect, useState } from 'react'; import { BehaviorSubject } from 'rxjs'; import { SecurityPageName } from '../../../common/constants'; -import { appLinks } from './app_links'; +import { getAllAppLinks } from './app_links'; import { AppLinkItems, LinkInfo, @@ -19,16 +20,38 @@ import { LinksPermissions, } from './types'; -export const appLinksUpdater$ = new BehaviorSubject(appLinks); +/** + * App links updater, it keeps the value of the app links in sync with all application. + * It can be updated using `updateAppLinks` or `updateAllAppLinks`. + * Read it using `subscribeAppLinks` or `useAppLinks` hook. + */ +const appLinksUpdater$ = new BehaviorSubject(getAllAppLinks()); + +export const useAppLinks = (): AppLinkItems => { + const [appLinks, setAppLinks] = useState(appLinksUpdater$.getValue()); + + useEffect(() => { + const linksSubscription = subscribeAppLinks((newAppLinks) => { + setAppLinks(newAppLinks); + }); + return () => linksSubscription.unsubscribe(); + }, []); -export const getAllAppLinks = () => appLinks; + return appLinks; +}; + +export const subscribeAppLinks = (onChange: (appItems: AppLinkItems) => void) => + appLinksUpdater$.subscribe(onChange); -export const updateAppLinks = (appLinksToUpdate: LinkItem[]) => { - appLinksUpdater$.next(Object.freeze(appLinksToUpdate)); +export const updateAppLinks = ( + appLinksToUpdate: AppLinkItems, + linksPermissions: LinksPermissions +) => { + appLinksUpdater$.next(Object.freeze(getFilteredAppLinks(appLinksToUpdate, linksPermissions))); }; -export const updateFilteredAppLinks = (linksPermissions: LinksPermissions) => { - updateAppLinks(getFilteredAppLinks(appLinks, linksPermissions)); +export const updateAllAppLinks = (linksPermissions: LinksPermissions) => { + updateAppLinks(getAllAppLinks(), linksPermissions); }; const getFilteredAppLinks = ( @@ -50,25 +73,6 @@ const getFilteredAppLinks = ( return acc; }, []); -// const createNavLinkItem = (link: LinkItem, linkProps?: UserPermissions): NavLinkItem => ({ -// id: link.id, -// path: link.path, -// title: link.title, -// ...(link.description != null ? { description: link.description } : {}), -// ...(link.icon != null ? { icon: link.icon } : {}), -// ...(link.image != null ? { image: link.image } : {}), -// ...(link.links && link.links.length -// ? { -// links: reduceLinks({ -// links: link.links, -// linkProps, -// formatFunction: createNavLinkItem, -// }), -// } -// : {}), -// ...(link.skipUrlState != null ? { skipUrlState: link.skipUrlState } : {}), -// }); - // It checks if the user has at least one of the link capabilities needed const hasCapabilities = (linkCapabilities: string[], userCapabilities: Capabilities): boolean => linkCapabilities.some((linkCapability) => get(userCapabilities, linkCapability, false)); @@ -126,7 +130,9 @@ const getNormalizedLinks = ( /** * Normalized indexed version of the global `links` array, referencing the parent by id, instead of having nested links children */ -const normalizedLinks: Readonly = Object.freeze(getNormalizedLinks(appLinks)); +const normalizedLinks: Readonly = Object.freeze( + getNormalizedLinks(getAllAppLinks()) +); /** * Returns the `NormalizedLink` from a link id parameter. diff --git a/x-pack/plugins/security_solution/public/common/links/types.ts b/x-pack/plugins/security_solution/public/common/links/types.ts index 3fa910c31a48a..0607db5a207af 100644 --- a/x-pack/plugins/security_solution/public/common/links/types.ts +++ b/x-pack/plugins/security_solution/public/common/links/types.ts @@ -12,29 +12,54 @@ import { ExperimentalFeatures } from '../../../common/experimental_features'; import { SecurityPageName } from '../../../common/constants'; type UiSettings = Readonly>; + +/** + * Permissions related parameters needed for the links to be filtered + */ export interface LinksPermissions { - experimentalFeatures: Readonly; capabilities: Capabilities; - uiSettings: UiSettings; + experimentalFeatures: Readonly; license?: ILicense; + uiSettings: UiSettings; } export interface LinkItem { + /** + * The description of the link content + */ description?: string; - disabled?: boolean; // default false /** - * Displays deep link when feature flag is enabled. + * Experimental flag needed to enable the link */ experimentalKey?: keyof ExperimentalFeatures; + /** + * Capabilities strings to enable the link. + * Uses "or" conditional, only one capability needed to enable the link + */ capabilities?: string[]; /** - * Hides deep link when feature flag is enabled. + * Enables link in the global navigation. Defaults to false. + */ + globalNavEnabled?: boolean; + /** + * Global navigation order number */ - globalNavEnabled?: boolean; // default false globalNavOrder?: number; + /** + * Enables link in the global search. Defaults to true. + */ globalSearchEnabled?: boolean; + /** + * Keywords for the global search to search. + */ globalSearchKeywords?: string[]; + /** + * Experimental flag needed to disable the link. Opposite of experimentalKey + */ hideWhenExperimentalKey?: keyof ExperimentalFeatures; + /** + * Link id. Refers to a SecurityPageName + */ id: SecurityPageName; /** * Icon that is displayed on menu navigation landing page. @@ -46,28 +71,39 @@ export interface LinkItem { * Only required for pages that are displayed inside a landing page. */ landingImage?: string; - isBeta?: boolean; + /** + * Minimum licence required to enable the link + */ licenseType?: LicenseType; + /** + * Nested links + */ links?: LinkItem[]; + /** + * Link path relative to security root + */ path: string; + /** + * Disables link in the side navigation. Defaults to false. + */ + sideNavDisabled?: boolean; + /** + * Enables link in the side navigation. Defaults to false. + */ skipUrlState?: boolean; // defaults to false + /** + * Title of the link + */ title: string; + /** + * Callback to filter the link based in uiSettings (kibana advanced settings) + * Return false to exclude the link + */ uiSettingsEnabled?: (uiSettings: UiSettings) => boolean; } export type AppLinkItems = Readonly; -export interface NavLinkItem { - description?: string; - icon?: IconType; - id: SecurityPageName; - links?: NavLinkItem[]; - image?: string; - path: string; - title: string; - skipUrlState?: boolean; // default to false -} - export type LinkInfo = Omit; export type NormalizedLink = LinkInfo & { parentId?: SecurityPageName }; export type NormalizedLinks = Record; diff --git a/x-pack/plugins/security_solution/public/landing_pages/pages/manage.tsx b/x-pack/plugins/security_solution/public/landing_pages/pages/manage.tsx index f0e6094d5113f..29e2c2715e98e 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/pages/manage.tsx +++ b/x-pack/plugins/security_solution/public/landing_pages/pages/manage.tsx @@ -22,7 +22,7 @@ export const ManageLandingPage = () => ( - + ); diff --git a/x-pack/plugins/security_solution/public/management/links.ts b/x-pack/plugins/security_solution/public/management/links.ts index 128a4732301f6..87b6b8f458865 100644 --- a/x-pack/plugins/security_solution/public/management/links.ts +++ b/x-pack/plugins/security_solution/public/management/links.ts @@ -12,7 +12,7 @@ import { EVENT_FILTERS_PATH, EXCEPTIONS_PATH, HOST_ISOLATION_EXCEPTIONS_PATH, - MANAGEMENT_PATH, + MANAGE_PATH, POLICIES_PATH, RULES_PATH, SecurityPageName, @@ -46,7 +46,7 @@ const FIX_ME_TEMPORARY_DESCRIPTION = 'Description here'; export const links: LinkItem = { id: SecurityPageName.administration, title: MANAGE, - path: MANAGEMENT_PATH, + path: MANAGE_PATH, skipUrlState: true, globalNavEnabled: false, capabilities: [`${SERVER_APP_ID}.show`], diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index e44a3b935e765..b04a0a61e94a4 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -64,7 +64,7 @@ import { import { LazyEndpointCustomAssetsExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_custom_assets_extension'; import { initDataView, SourcererModel, KibanaDataView } from './common/store/sourcerer/model'; import { SecurityDataView } from './common/containers/sourcerer/api'; -import { updateFilteredAppLinks } from './common/links'; +import { updateAllAppLinks } from './common/links'; import { LinksPermissions } from './common/links/types'; export class Plugin implements IPlugin { @@ -240,13 +240,13 @@ export class Plugin implements IPlugin { if (currentLicense.type !== undefined) { - updateFilteredAppLinks({ ...linksPermissions, license: currentLicense }); + updateAllAppLinks({ ...linksPermissions, license: currentLicense }); } else { - updateFilteredAppLinks(linksPermissions); + updateAllAppLinks(linksPermissions); } }); } else { - updateFilteredAppLinks(linksPermissions); + updateAllAppLinks(linksPermissions); } return {}; diff --git a/x-pack/plugins/security_solution/public/timelines/links.ts b/x-pack/plugins/security_solution/public/timelines/links.ts index b7cf4a818e111..bd972efd8a02a 100644 --- a/x-pack/plugins/security_solution/public/timelines/links.ts +++ b/x-pack/plugins/security_solution/public/timelines/links.ts @@ -29,6 +29,7 @@ export const links: LinkItem = { defaultMessage: 'Templates', }), path: `${TIMELINES_PATH}/template`, + sideNavDisabled: true, }, ], }; From 7e5895b137658ffe80c3eda2053c3b568fe7b40f Mon Sep 17 00:00:00 2001 From: semd Date: Mon, 16 May 2022 16:45:58 +0200 Subject: [PATCH 05/25] test and type fixes --- .../public/app/deep_links/index.ts | 2 +- .../app/home/template_wrapper/index.tsx | 19 +- .../components/navigation/nav_links.test.ts | 51 +- .../solution_grouped_nav.test.tsx | 4 +- .../solution_grouped_nav.tsx | 2 +- .../solution_grouped_nav_panel.test.tsx | 18 +- .../public/common/links/links.test.ts | 611 +++++++----------- .../public/common/links/links.ts | 7 +- .../public/common/links/types.ts | 16 +- .../components/landing_links_icons.test.tsx | 3 +- .../landing_pages/pages/manage.test.tsx | 5 +- .../security_solution/public/plugin.tsx | 2 - 12 files changed, 310 insertions(+), 430 deletions(-) diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.ts index 591c020899021..aece47eacc0cf 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.ts @@ -564,7 +564,7 @@ const formatDeepLinks = (appLinks: AppLinkItems): AppDeepLink[] => navLinkStatus: appLink.globalNavEnabled ? AppNavLinkStatus.visible : AppNavLinkStatus.hidden, ...(appLink.globalSearchKeywords != null ? { keywords: appLink.globalSearchKeywords } : {}), ...(appLink.globalNavOrder != null ? { order: appLink.globalNavOrder } : {}), - ...(appLink.globalSearchEnabled != null ? { searchable: appLink.globalSearchEnabled } : {}), + ...(appLink.globalSearchEnabled !== false ? { searchable: appLink.globalSearchEnabled } : {}), ...(appLink.links && appLink.links?.length ? { deepLinks: formatDeepLinks(appLink.links), diff --git a/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx b/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx index 3425485582ecf..5fa6403e548eb 100644 --- a/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx @@ -45,7 +45,7 @@ const NO_DATA_PAGE_TEMPLATE_PROPS = { */ const StyledKibanaPageTemplate = styled(KibanaPageTemplate)<{ $isShowingTimelineOverlay?: boolean; - $isBottomBarVisible?: boolean; + $addBottomPadding?: boolean; $isGroupedNav?: boolean; }>` .${BOTTOM_BAR_CLASSNAME} { @@ -64,8 +64,8 @@ const StyledKibanaPageTemplate = styled(KibanaPageTemplate)<{ } // If the bottom bar is visible add padding to the navigation - ${({ $isBottomBarVisible, $isGroupedNav }) => - ($isBottomBarVisible || $isGroupedNav) && + ${({ $addBottomPadding }) => + $addBottomPadding && ` @media (min-width: 768px) { .kbnPageTemplateSolutionNav { @@ -73,15 +73,6 @@ const StyledKibanaPageTemplate = styled(KibanaPageTemplate)<{ } } `} - - ${({ $isGroupedNav }) => - $isGroupedNav && - ` - .kbnPageTemplateSolutionNav { - display: flex; - flex-direction: column; - } - `} `; interface SecuritySolutionPageWrapperProps { @@ -98,6 +89,8 @@ export const SecuritySolutionTemplateWrapper: React.FC ({ - getNavLinkItems: () => mockNavLinks, + useAppLinks: () => mockNavLinks, })); const renderUseAppNavLinks = () => @@ -44,11 +49,47 @@ const renderUseAppRootNavLink = (id: SecurityPageName) => describe('useAppNavLinks', () => { it('should return all nav links', () => { const { result } = renderUseAppNavLinks(); - expect(result.current).toEqual(mockNavLinks); + expect(result.current).toMatchInlineSnapshot(` + Array [ + Object { + "description": "description", + "id": "administration", + "links": Array [ + Object { + "description": "description 2", + "disabled": true, + "icon": "someicon", + "id": "endpoints", + "image": "someimage", + "skipUrlState": true, + "title": "title 2", + }, + ], + "title": "title", + }, + ] + `); }); it('should return a root nav links', () => { const { result } = renderUseAppRootNavLink(SecurityPageName.administration); - expect(result.current).toEqual(mockNavLinks[0]); + expect(result.current).toMatchInlineSnapshot(` + Object { + "description": "description", + "id": "administration", + "links": Array [ + Object { + "description": "description 2", + "disabled": true, + "icon": "someicon", + "id": "endpoints", + "image": "someimage", + "skipUrlState": true, + "title": "title 2", + }, + ], + "title": "title", + } + `); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.test.tsx index f141264bd97e4..e41b566bbc7c8 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.test.tsx @@ -9,15 +9,15 @@ import React from 'react'; import { render, waitFor } from '@testing-library/react'; import { SecurityPageName } from '../../../../app/types'; import { TestProviders } from '../../../mock'; -import { NavItem } from './solution_grouped_nav_item'; import { SolutionGroupedNav, SolutionGroupedNavProps } from './solution_grouped_nav'; +import { SideNavItem } from './types'; const mockUseShowTimeline = jest.fn((): [boolean] => [false]); jest.mock('../../../utils/timeline/use_show_timeline', () => ({ useShowTimeline: () => mockUseShowTimeline(), })); -const mockItems: NavItem[] = [ +const mockItems: SideNavItem[] = [ { id: SecurityPageName.dashboardsLanding, label: 'Dashboards', diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.tsx index b1a954ca822ba..dbd742e551f24 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.tsx @@ -113,7 +113,7 @@ export const SolutionGroupedNavComponent: React.FC = ({ return ( <> - + diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_panel.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_panel.test.tsx index 93d46c35d6bed..21a620d9e2e9a 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_panel.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_panel.test.tsx @@ -9,18 +9,15 @@ import React from 'react'; import { render, waitFor } from '@testing-library/react'; import { SecurityPageName } from '../../../../app/types'; import { TestProviders } from '../../../mock'; -import { PortalNavItem } from './solution_grouped_nav_item'; -import { - SolutionGroupedNavPanel, - SolutionGroupedNavPanelProps, -} from './solution_grouped_nav_panel'; +import { SolutionNavPanel, SolutionNavPanelProps } from './solution_grouped_nav_panel'; +import { DefaultSideNavItem } from './types'; const mockUseShowTimeline = jest.fn((): [boolean] => [false]); jest.mock('../../../utils/timeline/use_show_timeline', () => ({ useShowTimeline: () => mockUseShowTimeline(), })); -const mockItems: PortalNavItem[] = [ +const mockItems: DefaultSideNavItem[] = [ { id: SecurityPageName.hosts, label: 'Hosts', @@ -37,16 +34,11 @@ const mockItems: PortalNavItem[] = [ const PANEL_TITLE = 'test title'; const mockOnClose = jest.fn(); -const renderNavPanel = (props: Partial = {}) => +const renderNavPanel = (props: Partial = {}) => render( <>
- + , { wrapper: TestProviders, diff --git a/x-pack/plugins/security_solution/public/common/links/links.test.ts b/x-pack/plugins/security_solution/public/common/links/links.test.ts index 1c5f8d2d9f2ab..2f152dff602bc 100644 --- a/x-pack/plugins/security_solution/public/common/links/links.test.ts +++ b/x-pack/plugins/security_solution/public/common/links/links.test.ts @@ -5,397 +5,266 @@ * 2.0. */ +import { CASES_FEATURE_ID, SecurityPageName, SERVER_APP_ID } from '../../../common/constants'; +import { Capabilities } from '@kbn/core/types'; +import { mockGlobalState, TestProviders } from '../mock'; +import { ILicense, LicenseType } from '@kbn/licensing-plugin/common/types'; +import { AppLinkItems } from './types'; +import { act, renderHook } from '@testing-library/react-hooks'; import { + useAppLinks, getAncestorLinksInfo, - getDeepLinks, - getInitialDeepLinks, getLinkInfo, - getNavLinkItems, needsUrlState, + updateAllAppLinks, + updateAppLinks, } from './links'; -import { CASES_FEATURE_ID, SecurityPageName, SERVER_APP_ID } from '../../../common/constants'; -import { Capabilities } from '@kbn/core/types'; -import { AppDeepLink } from '@kbn/core/public'; -import { mockGlobalState } from '../mock'; -import { NavLinkItem } from './types'; -import { LicenseType } from '@kbn/licensing-plugin/common/types'; -import { LicenseService } from '../../../common/license'; + +jest.mock('./app_links', () => ({ + getAllAppLinks: () => [ + { + id: 'hosts', + title: 'Hosts', + path: '/hosts', + links: [ + { + id: 'hosts-authentications', + title: 'Authentications', + path: `/hosts/authentications`, + experimentalKey: 'nonExistingKey', + }, + { + id: 'hosts-events', + title: 'Events', + path: `/hosts/events`, + skipUrlState: true, + }, + ], + }, + ], +})); const mockExperimentalDefaults = mockGlobalState.app.enableExperimental; + const mockCapabilities = { [CASES_FEATURE_ID]: { read_cases: true, crud_cases: true }, [SERVER_APP_ID]: { show: true }, } as unknown as Capabilities; -const findDeepLink = (id: string, deepLinks: AppDeepLink[]): AppDeepLink | null => - deepLinks.reduce((deepLinkFound: AppDeepLink | null, deepLink) => { - if (deepLinkFound !== null) { - return deepLinkFound; - } - if (deepLink.id === id) { - return deepLink; - } - if (deepLink.deepLinks) { - return findDeepLink(id, deepLink.deepLinks); - } - return null; - }, null); - -const findNavLink = (id: SecurityPageName, navLinks: NavLinkItem[]): NavLinkItem | null => - navLinks.reduce((deepLinkFound: NavLinkItem | null, deepLink) => { - if (deepLinkFound !== null) { - return deepLinkFound; - } - if (deepLink.id === id) { - return deepLink; - } - if (deepLink.links) { - return findNavLink(id, deepLink.links); - } - return null; - }, null); - -// remove filter once new nav is live -const allPages = Object.values(SecurityPageName).filter( - (pageName) => - pageName !== SecurityPageName.explore && - pageName !== SecurityPageName.detections && - pageName !== SecurityPageName.investigate -); -const casesPages = [ - SecurityPageName.case, - SecurityPageName.caseConfigure, - SecurityPageName.caseCreate, -]; -const featureFlagPages = [ - SecurityPageName.detectionAndResponse, - SecurityPageName.hostsAuthentications, - SecurityPageName.hostsRisk, - SecurityPageName.usersRisk, -]; -const premiumPages = [ - SecurityPageName.caseConfigure, - SecurityPageName.hostsAnomalies, - SecurityPageName.networkAnomalies, - SecurityPageName.usersAnomalies, - SecurityPageName.detectionAndResponse, - SecurityPageName.hostsRisk, - SecurityPageName.usersRisk, -]; -const nonCasesPages = allPages.reduce( - (acc: SecurityPageName[], p) => - casesPages.includes(p) || featureFlagPages.includes(p) ? acc : [p, ...acc], - [] -); - const licenseBasicMock = jest.fn().mockImplementation((arg: LicenseType) => arg === 'basic'); const licensePremiumMock = jest.fn().mockReturnValue(true); const mockLicense = { - isAtLeast: licensePremiumMock, -} as unknown as LicenseService; + hasAtLeast: licensePremiumMock, +} as unknown as ILicense; -describe('security app link helpers', () => { +const renderUseAppLinks = () => + renderHook<{}, AppLinkItems>(() => useAppLinks(), { wrapper: TestProviders }); + +describe('Security app links', () => { beforeEach(() => { - mockLicense.isAtLeast = licensePremiumMock; + mockLicense.hasAtLeast = licensePremiumMock; }); - describe('getInitialDeepLinks', () => { - it('should return all pages in the app', () => { - const links = getInitialDeepLinks(); - allPages.forEach((page) => expect(findDeepLink(page, links)).toBeTruthy()); - }); - }); - describe('getDeepLinks', () => { - it('basicLicense should return only basic links', async () => { - mockLicense.isAtLeast = licenseBasicMock; - const links = await getDeepLinks({ - enableExperimental: mockExperimentalDefaults, - license: mockLicense, - capabilities: mockCapabilities, - }); - expect(findDeepLink(SecurityPageName.hostsAnomalies, links)).toBeFalsy(); - allPages.forEach((page) => { - if (premiumPages.includes(page)) { - return expect(findDeepLink(page, links)).toBeFalsy(); - } - if (featureFlagPages.includes(page)) { - // ignore feature flag pages - return; - } - expect(findDeepLink(page, links)).toBeTruthy(); - }); - }); - it('platinumLicense should return all links', async () => { - const links = await getDeepLinks({ - enableExperimental: mockExperimentalDefaults, - license: mockLicense, - capabilities: mockCapabilities, - }); - allPages.forEach((page) => { - if (premiumPages.includes(page) && !featureFlagPages.includes(page)) { - return expect(findDeepLink(page, links)).toBeTruthy(); - } - if (featureFlagPages.includes(page)) { - // ignore feature flag pages - return; - } - expect(findDeepLink(page, links)).toBeTruthy(); - }); - }); - it('hideWhenExperimentalKey hides entry when key = true', async () => { - const links = await getDeepLinks({ - enableExperimental: { ...mockExperimentalDefaults, usersEnabled: true }, - license: mockLicense, - capabilities: mockCapabilities, - }); - expect(findDeepLink(SecurityPageName.hostsAuthentications, links)).toBeFalsy(); - }); - it('hideWhenExperimentalKey shows entry when key = false', async () => { - const links = await getDeepLinks({ - enableExperimental: { ...mockExperimentalDefaults, usersEnabled: false }, - license: mockLicense, - capabilities: mockCapabilities, - }); - expect(findDeepLink(SecurityPageName.hostsAuthentications, links)).toBeTruthy(); - }); - it('experimentalKey shows entry when key = false', async () => { - const links = await getDeepLinks({ - enableExperimental: { - ...mockExperimentalDefaults, - riskyHostsEnabled: false, - riskyUsersEnabled: false, - detectionResponseEnabled: false, - }, - license: mockLicense, - capabilities: mockCapabilities, - }); - expect(findDeepLink(SecurityPageName.hostsRisk, links)).toBeFalsy(); - expect(findDeepLink(SecurityPageName.usersRisk, links)).toBeFalsy(); - expect(findDeepLink(SecurityPageName.detectionAndResponse, links)).toBeFalsy(); - }); - it('experimentalKey shows entry when key = true', async () => { - const links = await getDeepLinks({ - enableExperimental: { - ...mockExperimentalDefaults, - riskyHostsEnabled: true, - riskyUsersEnabled: true, - detectionResponseEnabled: true, - }, - license: mockLicense, - capabilities: mockCapabilities, - }); - expect(findDeepLink(SecurityPageName.hostsRisk, links)).toBeTruthy(); - expect(findDeepLink(SecurityPageName.usersRisk, links)).toBeTruthy(); - expect(findDeepLink(SecurityPageName.detectionAndResponse, links)).toBeTruthy(); + describe('useAppLinks', () => { + it('should return initial appLinks', () => { + const { result } = renderUseAppLinks(); + expect(result.current).toMatchInlineSnapshot(` + Array [ + Object { + "id": "hosts", + "links": Array [ + Object { + "experimentalKey": "nonExistingKey", + "id": "hosts-authentications", + "path": "/hosts/authentications", + "title": "Authentications", + }, + Object { + "id": "hosts-events", + "path": "/hosts/events", + "skipUrlState": true, + "title": "Events", + }, + ], + "path": "/hosts", + "title": "Hosts", + }, + ] + `); }); - it('Removes siem features when siem capabilities are false', async () => { - const capabilities = { - ...mockCapabilities, - [SERVER_APP_ID]: { show: false }, - } as unknown as Capabilities; - const links = await getDeepLinks({ - enableExperimental: mockExperimentalDefaults, - license: mockLicense, - capabilities, - }); - nonCasesPages.forEach((page) => { - // investigate is active for both Cases and Timelines pages - if (page === SecurityPageName.investigate) { - return expect(findDeepLink(page, links)).toBeTruthy(); - } - return expect(findDeepLink(page, links)).toBeFalsy(); + it('should update all appLinks with filtering', async () => { + const { result, waitForNextUpdate } = renderUseAppLinks(); + await act(async () => { + updateAllAppLinks({ + capabilities: mockCapabilities, + experimentalFeatures: mockExperimentalDefaults, + license: mockLicense, + }); + await waitForNextUpdate(); }); - casesPages.forEach((page) => expect(findDeepLink(page, links)).toBeTruthy()); + expect(result.current).toMatchInlineSnapshot(` + Array [ + Object { + "id": "hosts", + "links": Array [ + Object { + "id": "hosts-events", + "path": "/hosts/events", + "skipUrlState": true, + "title": "Events", + }, + ], + "path": "/hosts", + "title": "Hosts", + }, + ] + `); }); - it('Removes cases features when cases capabilities are false', async () => { - const capabilities = { - ...mockCapabilities, - [CASES_FEATURE_ID]: { read_cases: false, crud_cases: false }, - } as unknown as Capabilities; - const links = await getDeepLinks({ - enableExperimental: mockExperimentalDefaults, - license: mockLicense, - capabilities, - }); - nonCasesPages.forEach((page) => expect(findDeepLink(page, links)).toBeTruthy()); - casesPages.forEach((page) => expect(findDeepLink(page, links)).toBeFalsy()); - }); - }); - describe('getNavLinkItems', () => { - it('basicLicense should return only basic links', () => { - mockLicense.isAtLeast = licenseBasicMock; - const links = getNavLinkItems({ - enableExperimental: mockExperimentalDefaults, - license: mockLicense, - capabilities: mockCapabilities, - }); - expect(findNavLink(SecurityPageName.hostsAnomalies, links)).toBeFalsy(); - allPages.forEach((page) => { - if (premiumPages.includes(page)) { - return expect(findNavLink(page, links)).toBeFalsy(); - } - if (featureFlagPages.includes(page)) { - // ignore feature flag pages - return; - } - expect(findNavLink(page, links)).toBeTruthy(); - }); - }); - it('platinumLicense should return all links', () => { - const links = getNavLinkItems({ - enableExperimental: mockExperimentalDefaults, - license: mockLicense, - capabilities: mockCapabilities, - }); - allPages.forEach((page) => { - if (premiumPages.includes(page) && !featureFlagPages.includes(page)) { - return expect(findNavLink(page, links)).toBeTruthy(); - } - if (featureFlagPages.includes(page)) { - // ignore feature flag pages - return; - } - expect(findNavLink(page, links)).toBeTruthy(); + it('should manually update appLinks with filtering', async () => { + const { result, waitForNextUpdate } = renderUseAppLinks(); + await act(async () => { + updateAppLinks( + [ + { + id: SecurityPageName.network, + title: 'Network', + path: '/network', + }, + ], + { + capabilities: mockCapabilities, + experimentalFeatures: mockExperimentalDefaults, + license: mockLicense, + } + ); + await waitForNextUpdate(); }); - }); - it('hideWhenExperimentalKey hides entry when key = true', () => { - const links = getNavLinkItems({ - enableExperimental: { ...mockExperimentalDefaults, usersEnabled: true }, - license: mockLicense, - capabilities: mockCapabilities, - }); - expect(findNavLink(SecurityPageName.hostsAuthentications, links)).toBeFalsy(); - }); - it('hideWhenExperimentalKey shows entry when key = false', () => { - const links = getNavLinkItems({ - enableExperimental: { ...mockExperimentalDefaults, usersEnabled: false }, - license: mockLicense, - capabilities: mockCapabilities, - }); - expect(findNavLink(SecurityPageName.hostsAuthentications, links)).toBeTruthy(); - }); - it('experimentalKey shows entry when key = false', () => { - const links = getNavLinkItems({ - enableExperimental: { - ...mockExperimentalDefaults, - riskyHostsEnabled: false, - riskyUsersEnabled: false, - detectionResponseEnabled: false, - }, - license: mockLicense, - capabilities: mockCapabilities, - }); - expect(findNavLink(SecurityPageName.hostsRisk, links)).toBeFalsy(); - expect(findNavLink(SecurityPageName.usersRisk, links)).toBeFalsy(); - expect(findNavLink(SecurityPageName.detectionAndResponse, links)).toBeFalsy(); - }); - it('experimentalKey shows entry when key = true', () => { - const links = getNavLinkItems({ - enableExperimental: { - ...mockExperimentalDefaults, - riskyHostsEnabled: true, - riskyUsersEnabled: true, - detectionResponseEnabled: true, - }, - license: mockLicense, - capabilities: mockCapabilities, - }); - expect(findNavLink(SecurityPageName.hostsRisk, links)).toBeTruthy(); - expect(findNavLink(SecurityPageName.usersRisk, links)).toBeTruthy(); - expect(findNavLink(SecurityPageName.detectionAndResponse, links)).toBeTruthy(); + expect(result.current).toMatchInlineSnapshot(` + Array [ + Object { + "id": "network", + "path": "/network", + "title": "Network", + }, + ] + `); }); - it('Removes siem features when siem capabilities are false', () => { - const capabilities = { - ...mockCapabilities, - [SERVER_APP_ID]: { show: false }, - } as unknown as Capabilities; - const links = getNavLinkItems({ - enableExperimental: mockExperimentalDefaults, - license: mockLicense, - capabilities, - }); - nonCasesPages.forEach((page) => { - // investigate is active for both Cases and Timelines pages - if (page === SecurityPageName.investigate) { - return expect(findNavLink(page, links)).toBeTruthy(); - } - return expect(findNavLink(page, links)).toBeFalsy(); - }); - casesPages.forEach((page) => expect(findNavLink(page, links)).toBeTruthy()); - }); - it('Removes cases features when cases capabilities are false', () => { - const capabilities = { - ...mockCapabilities, - [CASES_FEATURE_ID]: { read_cases: false, crud_cases: false }, - } as unknown as Capabilities; - const links = getNavLinkItems({ - enableExperimental: mockExperimentalDefaults, - license: mockLicense, - capabilities, + it('should filter not allowed links', async () => { + const { result, waitForNextUpdate } = renderUseAppLinks(); + await act(async () => { + updateAppLinks( + [ + { + // this link should not be excluded, the test checks all conditions passed + // all its sub-links will be filtered for different reasons + id: SecurityPageName.network, + title: 'Network', + path: '/network', + capabilities: [`${CASES_FEATURE_ID}.read_cases`, `${SERVER_APP_ID}.show`], + experimentalKey: 'flagEnabled' as unknown as keyof typeof mockExperimentalDefaults, + hideWhenExperimentalKey: + 'flagDisabled' as unknown as keyof typeof mockExperimentalDefaults, + licenseType: 'basic', + links: [ + { + id: SecurityPageName.networkExternalAlerts, + title: 'external alerts', + path: '/external_alerts', + experimentalKey: + 'flagDisabled' as unknown as keyof typeof mockExperimentalDefaults, + }, + { + id: SecurityPageName.networkDns, + title: 'dns', + path: '/dns', + hideWhenExperimentalKey: + 'flagEnabled' as unknown as keyof typeof mockExperimentalDefaults, + }, + { + id: SecurityPageName.networkAnomalies, + title: 'Anomalies', + path: '/anomalies', + capabilities: [ + `${CASES_FEATURE_ID}.read_cases`, + `${CASES_FEATURE_ID}.write_cases`, + ], + }, + { + id: SecurityPageName.networkHttp, + title: 'Http', + path: '/http', + licenseType: 'gold', + }, + ], + }, + { + id: SecurityPageName.hosts, + title: 'Hosts', + path: '/hosts', + licenseType: 'platinum', + links: [ + { + id: SecurityPageName.hostsEvents, + title: 'Events', + path: '/events', + }, + ], + }, + ], + { + capabilities: { + ...mockCapabilities, + [CASES_FEATURE_ID]: { read_cases: false, crud_cases: false }, + }, + experimentalFeatures: { + flagEnabled: true, + flagDisabled: false, + } as unknown as typeof mockExperimentalDefaults, + license: { hasAtLeast: licenseBasicMock } as unknown as ILicense, + } + ); + await waitForNextUpdate(); }); - nonCasesPages.forEach((page) => expect(findNavLink(page, links)).toBeTruthy()); - casesPages.forEach((page) => expect(findNavLink(page, links)).toBeFalsy()); + expect(result.current).toMatchInlineSnapshot(` + Array [ + Object { + "capabilities": Array [ + "securitySolutionCases.read_cases", + "siem.show", + ], + "experimentalKey": "flagEnabled", + "hideWhenExperimentalKey": "flagDisabled", + "id": "network", + "licenseType": "basic", + "path": "/network", + "title": "Network", + }, + ] + `); }); }); describe('getAncestorLinksInfo', () => { - it('finds flattened links for hosts', () => { - const hierarchy = getAncestorLinksInfo(SecurityPageName.hosts); - expect(hierarchy).toEqual([ - { - features: ['siem.show'], - globalNavEnabled: false, - globalSearchKeywords: ['Threat hunting'], - id: 'threat-hunting', - path: '/threat_hunting', - title: 'Threat Hunting', - }, - { - globalNavEnabled: true, - globalNavOrder: 9002, - globalSearchEnabled: true, - globalSearchKeywords: ['Hosts'], - id: 'hosts', - path: '/hosts', - title: 'Hosts', - landingImage: 'test-file-stub', - description: - 'A computer or other device that communicates with other hosts on a network. Hosts on a network include clients and servers -- that send or receive data, services or applications.', - }, - ]); - }); - it('finds flattened links for uncommonProcesses', () => { - const hierarchy = getAncestorLinksInfo(SecurityPageName.uncommonProcesses); - expect(hierarchy).toEqual([ - { - features: ['siem.show'], - globalNavEnabled: false, - globalSearchKeywords: ['Threat hunting'], - id: 'threat-hunting', - path: '/threat_hunting', - title: 'Threat Hunting', - }, - { - globalNavEnabled: true, - globalNavOrder: 9002, - globalSearchEnabled: true, - globalSearchKeywords: ['Hosts'], - id: 'hosts', - path: '/hosts', - title: 'Hosts', - landingImage: 'test-file-stub', - description: - 'A computer or other device that communicates with other hosts on a network. Hosts on a network include clients and servers -- that send or receive data, services or applications.', - }, - { - id: 'uncommon_processes', - path: '/hosts/uncommonProcesses', - title: 'Uncommon Processes', - }, - ]); + it('finds ancestors flattened links', () => { + const hierarchy = getAncestorLinksInfo(SecurityPageName.hostsEvents); + expect(hierarchy).toMatchInlineSnapshot(` + Array [ + Object { + "id": "hosts", + "path": "/hosts", + "title": "Hosts", + }, + Object { + "id": "hosts-events", + "path": "/hosts/events", + "skipUrlState": true, + "title": "Events", + }, + ] + `); }); }); @@ -405,26 +274,22 @@ describe('security app link helpers', () => { expect(needsUrl).toEqual(true); }); it('returns false when url state does not exist for page', () => { - const needsUrl = needsUrlState(SecurityPageName.landing); + const needsUrl = needsUrlState(SecurityPageName.hostsEvents); expect(needsUrl).toEqual(false); }); }); describe('getLinkInfo', () => { it('gets information for an individual link', () => { - const linkInfo = getLinkInfo(SecurityPageName.hosts); - expect(linkInfo).toEqual({ - globalNavEnabled: true, - globalNavOrder: 9002, - globalSearchEnabled: true, - globalSearchKeywords: ['Hosts'], - id: 'hosts', - path: '/hosts', - title: 'Hosts', - landingImage: 'test-file-stub', - description: - 'A computer or other device that communicates with other hosts on a network. Hosts on a network include clients and servers -- that send or receive data, services or applications.', - }); + const linkInfo = getLinkInfo(SecurityPageName.hostsEvents); + expect(linkInfo).toMatchInlineSnapshot(` + Object { + "id": "hosts-events", + "path": "/hosts/events", + "skipUrlState": true, + "title": "Events", + } + `); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/links/links.ts b/x-pack/plugins/security_solution/public/common/links/links.ts index 2f77dbd5ef43a..a2d06aca90b89 100644 --- a/x-pack/plugins/security_solution/public/common/links/links.ts +++ b/x-pack/plugins/security_solution/public/common/links/links.ts @@ -66,6 +66,8 @@ const getFilteredAppLinks = ( const childrenLinks = getFilteredAppLinks(links, linksPermissions); if (childrenLinks.length > 0) { acc.push({ ...appLink, links: childrenLinks }); + } else { + acc.push(appLink); } } else { acc.push(appLink); @@ -79,7 +81,7 @@ const hasCapabilities = (linkCapabilities: string[], userCapabilities: Capabilit const isLinkAllowed = ( link: LinkItem, - { license, experimentalFeatures, capabilities, uiSettings }: LinksPermissions + { license, experimentalFeatures, capabilities }: LinksPermissions ) => { const linkLicenseType = link.licenseType ?? 'basic'; if (license) { @@ -98,9 +100,6 @@ const isLinkAllowed = ( if (link.capabilities && !hasCapabilities(link.capabilities, capabilities)) { return false; } - if (link.uiSettingsEnabled && !link.uiSettingsEnabled(uiSettings)) { - return false; - } return true; }; diff --git a/x-pack/plugins/security_solution/public/common/links/types.ts b/x-pack/plugins/security_solution/public/common/links/types.ts index 0607db5a207af..aa607ad0dfbb9 100644 --- a/x-pack/plugins/security_solution/public/common/links/types.ts +++ b/x-pack/plugins/security_solution/public/common/links/types.ts @@ -5,14 +5,12 @@ * 2.0. */ -import { Capabilities, PublicUiSettingsParams, UserProvidedValues } from '@kbn/core/types'; +import { Capabilities } from '@kbn/core/types'; import { ILicense, LicenseType } from '@kbn/licensing-plugin/common/types'; import { IconType } from '@elastic/eui'; import { ExperimentalFeatures } from '../../../common/experimental_features'; import { SecurityPageName } from '../../../common/constants'; -type UiSettings = Readonly>; - /** * Permissions related parameters needed for the links to be filtered */ @@ -20,7 +18,6 @@ export interface LinksPermissions { capabilities: Capabilities; experimentalFeatures: Readonly; license?: ILicense; - uiSettings: UiSettings; } export interface LinkItem { @@ -61,6 +58,10 @@ export interface LinkItem { * Link id. Refers to a SecurityPageName */ id: SecurityPageName; + /** + * Displays the "Beta" badge + */ + isBeta?: boolean; /** * Icon that is displayed on menu navigation landing page. * Only required for pages that are displayed inside a landing page. @@ -72,7 +73,7 @@ export interface LinkItem { */ landingImage?: string; /** - * Minimum licence required to enable the link + * Minimum license required to enable the link */ licenseType?: LicenseType; /** @@ -95,11 +96,6 @@ export interface LinkItem { * Title of the link */ title: string; - /** - * Callback to filter the link based in uiSettings (kibana advanced settings) - * Return false to exclude the link - */ - uiSettingsEnabled?: (uiSettings: UiSettings) => boolean; } export type AppLinkItems = Readonly; diff --git a/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_icons.test.tsx b/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_icons.test.tsx index 81b72527500ad..57aee98af4e9d 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_icons.test.tsx +++ b/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_icons.test.tsx @@ -8,7 +8,7 @@ import { render } from '@testing-library/react'; import React from 'react'; import { SecurityPageName } from '../../app/types'; -import { NavLinkItem } from '../../common/links/types'; +import { NavLinkItem } from '../../common/components/navigation/types'; import { TestProviders } from '../../common/mock'; import { LandingLinksIcons } from './landing_links_icons'; @@ -17,7 +17,6 @@ const DEFAULT_NAV_ITEM: NavLinkItem = { title: 'TEST LABEL', description: 'TEST DESCRIPTION', icon: 'myTestIcon', - path: '', }; const mockNavigateTo = jest.fn(); diff --git a/x-pack/plugins/security_solution/public/landing_pages/pages/manage.test.tsx b/x-pack/plugins/security_solution/public/landing_pages/pages/manage.test.tsx index 1955d56c0a151..180517f93605e 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/pages/manage.test.tsx +++ b/x-pack/plugins/security_solution/public/landing_pages/pages/manage.test.tsx @@ -10,14 +10,13 @@ import React from 'react'; import { SecurityPageName } from '../../app/types'; import { TestProviders } from '../../common/mock'; import { LandingCategories } from './manage'; -import { NavLinkItem } from '../../common/links/types'; +import { NavLinkItem } from '../../common/components/navigation/types'; const RULES_ITEM_LABEL = 'elastic rules!'; const EXCEPTIONS_ITEM_LABEL = 'exceptional!'; const mockAppManageLink: NavLinkItem = { id: SecurityPageName.administration, - path: '', title: 'admin', links: [ { @@ -25,14 +24,12 @@ const mockAppManageLink: NavLinkItem = { title: RULES_ITEM_LABEL, description: '', icon: 'testIcon1', - path: '', }, { id: SecurityPageName.exceptions, title: EXCEPTIONS_ITEM_LABEL, description: '', icon: 'testIcon2', - path: '', }, ], }; diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index b04a0a61e94a4..73238369d4673 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -224,12 +224,10 @@ export class Plugin implements IPlugin Date: Mon, 16 May 2022 14:51:54 +0000 Subject: [PATCH 06/25] [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix' --- .../navigation/solution_grouped_nav/solution_grouped_nav.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.tsx index dbd742e551f24..0ebff576487ce 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.tsx @@ -113,7 +113,7 @@ export const SolutionGroupedNavComponent: React.FC = ({ return ( <> - + From 01a3fa267f52f96ffb426faa9cfc2521ae475292 Mon Sep 17 00:00:00 2001 From: semd Date: Mon, 16 May 2022 18:33:26 +0200 Subject: [PATCH 07/25] types changes --- .../src/page_template/solution_nav/solution_nav.scss | 2 +- .../public/landing_pages/components/landing_links_icons.tsx | 2 +- .../landing_pages/components/landing_links_images.test.tsx | 3 +-- .../public/landing_pages/components/landing_links_images.tsx | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/kbn-shared-ux-components/src/page_template/solution_nav/solution_nav.scss b/packages/kbn-shared-ux-components/src/page_template/solution_nav/solution_nav.scss index 3c520828de3ea..04c79bff9ef9d 100644 --- a/packages/kbn-shared-ux-components/src/page_template/solution_nav/solution_nav.scss +++ b/packages/kbn-shared-ux-components/src/page_template/solution_nav/solution_nav.scss @@ -18,7 +18,7 @@ $euiSideNavEmphasizedBackgroundColor: transparentize($euiColorLightShade, .7); .kbnPageTemplateSolutionNav__avatar { margin-right: $euiSize; } - + display: flex; flex-direction: column; } diff --git a/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_icons.tsx b/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_icons.tsx index 04a3e20b1f178..b30d4f404b163 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_icons.tsx +++ b/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_icons.tsx @@ -12,7 +12,7 @@ import { SecuritySolutionLinkAnchor, withSecuritySolutionLink, } from '../../common/components/links'; -import { NavLinkItem } from '../../common/links/types'; +import { NavLinkItem } from '../../common/components/navigation/types'; interface LandingLinksImagesProps { items: NavLinkItem[]; diff --git a/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_images.test.tsx b/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_images.test.tsx index c44374852f29b..81881a3796f0b 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_images.test.tsx +++ b/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_images.test.tsx @@ -8,7 +8,7 @@ import { render } from '@testing-library/react'; import React from 'react'; import { SecurityPageName } from '../../app/types'; -import { NavLinkItem } from '../../common/links/types'; +import { NavLinkItem } from '../../common/components/navigation/types'; import { TestProviders } from '../../common/mock'; import { LandingLinksImages } from './landing_links_images'; @@ -17,7 +17,6 @@ const DEFAULT_NAV_ITEM: NavLinkItem = { title: 'TEST LABEL', description: 'TEST DESCRIPTION', image: 'TEST_IMAGE.png', - path: '', }; jest.mock('../../common/lib/kibana/kibana_react', () => { diff --git a/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_images.tsx b/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_images.tsx index 22bcc0f1aa251..4cf8db26bbe7a 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_images.tsx +++ b/x-pack/plugins/security_solution/public/landing_pages/components/landing_links_images.tsx @@ -8,7 +8,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiImage, EuiPanel, EuiText, EuiTitle } from import React from 'react'; import styled from 'styled-components'; import { withSecuritySolutionLink } from '../../common/components/links'; -import { NavLinkItem } from '../../common/links/types'; +import { NavLinkItem } from '../../common/components/navigation/types'; interface LandingLinksImagesProps { items: NavLinkItem[]; From e1f6a98c666a9b9892cb208d9c37baf17bef67bb Mon Sep 17 00:00:00 2001 From: semd Date: Mon, 16 May 2022 18:38:55 +0200 Subject: [PATCH 08/25] shared style change moved to a separate PR --- .../src/page_template/solution_nav/solution_nav.scss | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/kbn-shared-ux-components/src/page_template/solution_nav/solution_nav.scss b/packages/kbn-shared-ux-components/src/page_template/solution_nav/solution_nav.scss index 04c79bff9ef9d..91b96641047e8 100644 --- a/packages/kbn-shared-ux-components/src/page_template/solution_nav/solution_nav.scss +++ b/packages/kbn-shared-ux-components/src/page_template/solution_nav/solution_nav.scss @@ -18,9 +18,6 @@ $euiSideNavEmphasizedBackgroundColor: transparentize($euiColorLightShade, .7); .kbnPageTemplateSolutionNav__avatar { margin-right: $euiSize; } - - display: flex; - flex-direction: column; } .kbnPageTemplateSolutionNav--hidden { From d2b71de0c0fccaf8b6b4877d06456548d200b0b5 Mon Sep 17 00:00:00 2001 From: semd Date: Mon, 16 May 2022 19:24:00 +0200 Subject: [PATCH 09/25] use old deep links --- .../app/home/template_wrapper/index.tsx | 2 -- .../public/app/translations.ts | 6 ++-- .../solution_grouped_nav.tsx | 2 +- .../security_solution/public/plugin.tsx | 33 ++++++++++--------- 4 files changed, 22 insertions(+), 21 deletions(-) diff --git a/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx b/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx index 5fa6403e548eb..8d7d9daad550d 100644 --- a/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx @@ -46,7 +46,6 @@ const NO_DATA_PAGE_TEMPLATE_PROPS = { const StyledKibanaPageTemplate = styled(KibanaPageTemplate)<{ $isShowingTimelineOverlay?: boolean; $addBottomPadding?: boolean; - $isGroupedNav?: boolean; }>` .${BOTTOM_BAR_CLASSNAME} { animation: 'none !important'; // disable the default bottom bar slide animation @@ -112,7 +111,6 @@ export const SecuritySolutionTemplateWrapper: React.FC diff --git a/x-pack/plugins/security_solution/public/app/translations.ts b/x-pack/plugins/security_solution/public/app/translations.ts index e9a45c0397316..aa7eaa83685db 100644 --- a/x-pack/plugins/security_solution/public/app/translations.ts +++ b/x-pack/plugins/security_solution/public/app/translations.ts @@ -43,7 +43,7 @@ export const USERS = i18n.translate('xpack.securitySolution.navigation.users', { }); export const RULES = i18n.translate('xpack.securitySolution.navigation.rules', { - defaultMessage: 'SIEM rules', + defaultMessage: 'Rules', }); export const EXCEPTIONS = i18n.translate('xpack.securitySolution.navigation.exceptions', { @@ -71,7 +71,7 @@ export const ENDPOINTS = i18n.translate('xpack.securitySolution.search.administr export const POLICIES = i18n.translate( 'xpack.securitySolution.navigation.administration.policies', { - defaultMessage: 'Endpoint policies', + defaultMessage: 'Policies', } ); export const TRUSTED_APPLICATIONS = i18n.translate( @@ -90,7 +90,7 @@ export const EVENT_FILTERS = i18n.translate( export const HOST_ISOLATION_EXCEPTIONS = i18n.translate( 'xpack.securitySolution.search.administration.hostIsolationExceptions', { - defaultMessage: 'Host isolation IP exceptions', + defaultMessage: 'Host isolation exceptions', } ); export const DETECT = i18n.translate('xpack.securitySolution.navigation.detect', { diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.tsx index 0ebff576487ce..b1a954ca822ba 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.tsx @@ -113,7 +113,7 @@ export const SolutionGroupedNavComponent: React.FC = ({ return ( <> - + diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 73238369d4673..4b49c04f295a5 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -47,7 +47,7 @@ import { SOURCERER_API_URL, } from '../common/constants'; -import { getAllDeepLinks, registerDeepLinksUpdater } from './app/deep_links'; +import { getDeepLinks } from './app/deep_links'; import { getSubPluginRoutesByCapabilities, manageOldSiemRoutes } from './helpers'; import { SecurityAppStore } from './common/store/store'; import { licenseService } from './common/hooks/use_license'; @@ -64,8 +64,6 @@ import { import { LazyEndpointCustomAssetsExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_custom_assets_extension'; import { initDataView, SourcererModel, KibanaDataView } from './common/store/sourcerer/model'; import { SecurityDataView } from './common/containers/sourcerer/api'; -import { updateAllAppLinks } from './common/links'; -import { LinksPermissions } from './common/links/types'; export class Plugin implements IPlugin { readonly kibanaVersion: string; @@ -142,7 +140,7 @@ export class Plugin implements IPlugin { // required to show the alert table inside cases const { alertsTableConfigurationRegistry } = plugins.triggersActionsUi; @@ -225,26 +223,31 @@ export class Plugin implements IPlugin { if (currentLicense.type !== undefined) { - updateAllAppLinks({ ...linksPermissions, license: currentLicense }); - } else { - updateAllAppLinks(linksPermissions); + this.appUpdater$.next(() => ({ + navLinkStatus: AppNavLinkStatus.hidden, // workaround to prevent main navLink to switch to visible after update. should not be needed + deepLinks: getDeepLinks( + this.experimentalFeatures, + currentLicense.type, + core.application.capabilities + ), + })); } }); } else { - updateAllAppLinks(linksPermissions); + this.appUpdater$.next(() => ({ + navLinkStatus: AppNavLinkStatus.hidden, // workaround to prevent main navLink to switch to visible after update. should not be needed + deepLinks: getDeepLinks( + this.experimentalFeatures, + undefined, + core.application.capabilities + ), + })); } return {}; From 320fce71d46c420a2642a76e39d7eed835bc2b31 Mon Sep 17 00:00:00 2001 From: semd Date: Tue, 17 May 2022 12:52:45 +0200 Subject: [PATCH 10/25] minor changes after ux meeting --- .../public/app/translations.ts | 2 +- .../security_side_nav/icons/launch.tsx | 18 +- .../security_side_nav.test.tsx | 160 ++++++++++++++++++ .../security_side_nav/security_side_nav.tsx | 54 ++++-- .../solution_grouped_nav.tsx | 2 +- .../solution_grouped_nav_panel.tsx | 47 +++-- .../navigation/solution_grouped_nav/types.ts | 2 +- .../__snapshots__/index.test.tsx.snap | 6 +- .../public/common/links/links.test.ts | 2 +- 9 files changed, 236 insertions(+), 57 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.test.tsx diff --git a/x-pack/plugins/security_solution/public/app/translations.ts b/x-pack/plugins/security_solution/public/app/translations.ts index aa7eaa83685db..32f7156d185c6 100644 --- a/x-pack/plugins/security_solution/public/app/translations.ts +++ b/x-pack/plugins/security_solution/public/app/translations.ts @@ -23,7 +23,7 @@ export const HOSTS = i18n.translate('xpack.securitySolution.navigation.hosts', { }); export const GETTING_STARTED = i18n.translate('xpack.securitySolution.navigation.gettingStarted', { - defaultMessage: 'Getting started', + defaultMessage: 'Get started', }); export const THREAT_HUNTING = i18n.translate('xpack.securitySolution.navigation.threatHunting', { diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/icons/launch.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/icons/launch.tsx index 90742327a7d04..de96338ef98e6 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/icons/launch.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/icons/launch.tsx @@ -16,18 +16,10 @@ export const EuiIconLaunch: React.FC> = ({ ...props }) = viewBox="0 0 16 16" {...props} > - - - - - + + + + + ); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.test.tsx new file mode 100644 index 0000000000000..f63ee1bd5e9c6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.test.tsx @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { SecurityPageName } from '../../../../app/types'; +import { TestProviders } from '../../../mock'; +import { SecuritySideNav } from './security_side_nav'; +import { SolutionGroupedNavProps } from '../solution_grouped_nav/solution_grouped_nav'; +import { navigationCategories as managementCategories } from '../../../../management/links'; +import { NavLinkItem } from '../types'; + +const mockSolutionGroupedNav = jest.fn((_: SolutionGroupedNavProps) => <>); +jest.mock('../solution_grouped_nav', () => ({ + SolutionGroupedNav: (props: SolutionGroupedNavProps) => mockSolutionGroupedNav(props), +})); +const mockUseRouteSpy = [{ pageName: SecurityPageName.alerts }]; +jest.mock('../../../utils/route/use_route_spy', () => ({ + useRouteSpy: () => mockUseRouteSpy, +})); + +const manageNavLink = { + id: SecurityPageName.administration, + title: 'manage', + description: 'manage description', + links: [ + { + id: SecurityPageName.endpoints, + title: 'title 2', + description: 'description 2', + }, + ], +}; +const alertsNavLink = { + id: SecurityPageName.alerts, + title: 'alerts', + description: 'alerts description', +}; + +const mockUseAppNavLinks = jest.fn((): NavLinkItem[] => [alertsNavLink]); +jest.mock('../nav_links', () => ({ + useAppNavLinks: () => mockUseAppNavLinks(), +})); + +jest.mock('../../links', () => ({ + useGetSecuritySolutionLinkProps: + () => + ({ deepLinkId }: { deepLinkId: SecurityPageName }) => ({ + href: `/${deepLinkId}`, + }), +})); + +const renderNav = () => + render(, { + wrapper: TestProviders, + }); + +describe('SecuritySideNav', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render main items', () => { + mockUseAppNavLinks.mockReturnValueOnce([alertsNavLink]); + renderNav(); + expect(mockSolutionGroupedNav).toHaveBeenCalledWith({ + selectedId: SecurityPageName.alerts, + items: [ + { + id: SecurityPageName.alerts, + label: 'alerts', + href: '/alerts', + }, + ], + footerItems: [], + }); + }); + + it('should render footer items', () => { + mockUseAppNavLinks.mockReturnValueOnce([manageNavLink]); + renderNav(); + expect(mockSolutionGroupedNav).toHaveBeenCalledWith( + expect.objectContaining({ + items: [], + selectedId: SecurityPageName.alerts, + footerItems: [ + { + id: SecurityPageName.administration, + label: 'manage', + href: '/administration', + categories: managementCategories, + items: [ + { + id: SecurityPageName.endpoints, + label: 'title 2', + description: 'description 2', + href: '/endpoints', + }, + ], + }, + ], + }) + ); + }); + it('should not render disabled items', () => { + mockUseAppNavLinks.mockReturnValueOnce([ + { ...alertsNavLink, disabled: true }, + { + ...manageNavLink, + links: [ + { + id: SecurityPageName.endpoints, + title: 'title 2', + description: 'description 2', + disabled: true, + }, + ], + }, + ]); + renderNav(); + expect(mockSolutionGroupedNav).toHaveBeenCalledWith( + expect.objectContaining({ + items: [], + selectedId: SecurityPageName.alerts, + footerItems: [ + { + id: SecurityPageName.administration, + label: 'manage', + href: '/administration', + categories: managementCategories, + items: [], + }, + ], + }) + ); + }); + + it('should render custom item', () => { + mockUseAppNavLinks.mockReturnValueOnce([ + { id: SecurityPageName.landing, title: 'get started' }, + ]); + renderNav(); + expect(mockSolutionGroupedNav).toHaveBeenCalledWith( + expect.objectContaining({ + items: [], + selectedId: SecurityPageName.alerts, + footerItems: [ + { + id: SecurityPageName.landing, + render: expect.any(Function), + }, + ], + }) + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.tsx index 41525f81d5207..296901c58c3ad 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import React, { useMemo, useCallback } from 'react'; -import { EuiLink, EuiListGroupItem } from '@elastic/eui'; +import React, { useMemo, useCallback, MouseEventHandler } from 'react'; +import { EuiHorizontalRule, EuiLink, EuiListGroupItem } from '@elastic/eui'; import { SecurityPageName } from '../../../../app/types'; import { navigationCategories as managementCategories } from '../../../../management/links'; import { getAncestorLinksInfo } from '../../../links'; @@ -23,6 +23,34 @@ const isFooterNavItem = (id: SecurityPageName) => type FormatSideNavItems = (navItems: NavLinkItem) => SideNavItem; +/** + * Renders the navigation item for "Get Started" custom link + */ +const GetStartedCustomLinkComponent: React.FC<{ + isSelected: boolean; + title: string; + href: string; + onClick: MouseEventHandler; +}> = ({ isSelected, title, href, onClick }) => ( + // eslint-disable-next-line @elastic/eui/href-or-on-click + + + + +); +const GetStartedCustomLink = React.memo(GetStartedCustomLinkComponent); + +/** + * Returns a function to format generic `NavLinkItem` array to the `SideNavItem` type + */ const useFormatSideNavItem = (): FormatSideNavItems => { const getSecuritySolutionLinkProps = useGetSecuritySolutionLinkProps(); // adds href and onClick props @@ -53,10 +81,12 @@ const useFormatSideNavItem = (): FormatSideNavItems => { const formatGetStartedItem = (navItem: NavLinkItem): CustomSideNavItem => ({ id: navItem.id, - render: () => ( - - - + render: (isSelected) => ( + ), }); @@ -74,6 +104,9 @@ const useFormatSideNavItem = (): FormatSideNavItems => { return formatSideNavItem; }; +/** + * Returns the formatted `items` and `footerItems` to be rendered in the navigation + */ const useSideNavItems = () => { const appNavLinks = useAppNavLinks(); const formatSideNavItem = useFormatSideNavItem(); @@ -108,11 +141,12 @@ const useSelectedId = (): SecurityPageName => { return selectedId; }; -const SecuritySideNavComponent: React.FC = () => { +/** + * Main security navigation component. + * It takes the links to render from the generic application `links` configs. + */ +export const SecuritySideNav: React.FC = () => { const [items, footerItems] = useSideNavItems(); const selectedId = useSelectedId(); return ; }; -SecuritySideNavComponent.displayName = 'SecuritySideNavComponent'; - -export const SecuritySideNav = SecuritySideNavComponent; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.tsx index b1a954ca822ba..70edb7afd37de 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.tsx @@ -180,7 +180,7 @@ const SolutionNavItemComponent: React.FC = ({ onOpenPanelNav, }) => { if (isCustomItem(item)) { - return {item.render()}; + return {item.render(isSelected)}; } const { id, href, label, onClick } = item; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_panel.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_panel.tsx index dafe18ed8aa53..2cdd2bbdf36fc 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_panel.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_panel.tsx @@ -121,13 +121,13 @@ const SolutionNavPanelCategories: React.FC = ({ items, onClose, }) => { - const itemsById = new Map(items.map((item) => [item.id, item])); + const itemsMap = new Map(items.map((item) => [item.id, item])); return ( <> {categories.map(({ label, linkIds }) => { const links = linkIds.reduce((acc, linkId) => { - const link = itemsById.get(linkId); + const link = itemsMap.get(linkId); if (link) { acc.push(link); } @@ -151,31 +151,24 @@ const SolutionNavPanelCategories: React.FC = ({ const SolutionNavPanelItems: React.FC = ({ items, onClose }) => ( <> - {items.map((item) => ( - + {items.map(({ id, href, onClick, label, description }) => ( + + + { + onClose(); + if (onClick) { + onClick(ev); + } + }} + > + {label} + + + {description} + ))} ); - -const SolutionNavPanelItem: React.FC = ({ - item: { id, href, onClick, label, description }, - onClose, -}) => ( - <> - - { - onClose(); - if (onClick) { - onClick(ev); - } - }} - > - {label} - - - {description} - -); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/types.ts b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/types.ts index e0b62d6fbd45d..3644c25d0493d 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/types.ts @@ -21,7 +21,7 @@ export interface DefaultSideNavItem { export interface CustomSideNavItem { id: string; - render: () => React.ReactNode; + render: (isSelected: boolean) => React.ReactNode; } export type SideNavItem = DefaultSideNavItem | CustomSideNavItem; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/__snapshots__/index.test.tsx.snap index b80e8e290f65b..d50b07ca56089 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/__snapshots__/index.test.tsx.snap @@ -14,7 +14,7 @@ Object { "href": "securitySolutionUI/get_started?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", "id": "get_started", "isSelected": false, - "name": "Getting started", + "name": "Get started", "onClick": [Function], }, Object { @@ -50,7 +50,7 @@ Object { "href": "securitySolutionUI/rules?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", "id": "rules", "isSelected": false, - "name": "SIEM rules", + "name": "Rules", "onClick": [Function], }, Object { @@ -148,7 +148,7 @@ Object { "href": "securitySolutionUI/host_isolation_exceptions", "id": "host_isolation_exceptions", "isSelected": false, - "name": "Host isolation IP exceptions", + "name": "Host isolation exceptions", "onClick": [Function], }, Object { diff --git a/x-pack/plugins/security_solution/public/common/links/links.test.ts b/x-pack/plugins/security_solution/public/common/links/links.test.ts index 2f152dff602bc..4524167b3b2c0 100644 --- a/x-pack/plugins/security_solution/public/common/links/links.test.ts +++ b/x-pack/plugins/security_solution/public/common/links/links.test.ts @@ -159,7 +159,7 @@ describe('Security app links', () => { [ { // this link should not be excluded, the test checks all conditions passed - // all its sub-links will be filtered for different reasons + // all its sub-links should be filtered for each criteria id: SecurityPageName.network, title: 'Network', path: '/network', From 66a7713205a31b9eeabfbcb525755fb22f01c6b2 Mon Sep 17 00:00:00 2001 From: semd Date: Tue, 17 May 2022 13:49:49 +0200 Subject: [PATCH 11/25] add links filtering --- .../security_solution/public/plugin.tsx | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 4b49c04f295a5..9934fb13e11b2 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -64,6 +64,7 @@ import { import { LazyEndpointCustomAssetsExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_custom_assets_extension'; import { initDataView, SourcererModel, KibanaDataView } from './common/store/sourcerer/model'; import { SecurityDataView } from './common/containers/sourcerer/api'; +import { updateAllAppLinks } from './common/links'; export class Plugin implements IPlugin { readonly kibanaVersion: string; @@ -226,9 +227,23 @@ export class Plugin implements IPlugin { if (currentLicense.type !== undefined) { + updateAllAppLinks({ + experimentalFeatures: this.experimentalFeatures, + license: currentLicense, + capabilities: core.application.capabilities, + }); + + // TODO: to be removed this.appUpdater$.next(() => ({ navLinkStatus: AppNavLinkStatus.hidden, // workaround to prevent main navLink to switch to visible after update. should not be needed deepLinks: getDeepLinks( @@ -240,6 +255,12 @@ export class Plugin implements IPlugin ({ navLinkStatus: AppNavLinkStatus.hidden, // workaround to prevent main navLink to switch to visible after update. should not be needed deepLinks: getDeepLinks( From 5222d41e2005a8c56d17f473ff59fb4c780e2f53 Mon Sep 17 00:00:00 2001 From: semd Date: Tue, 17 May 2022 14:08:27 +0200 Subject: [PATCH 12/25] remove duplicated categories --- .../public/landing_pages/constants.ts | 36 ------------------- 1 file changed, 36 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/landing_pages/constants.ts diff --git a/x-pack/plugins/security_solution/public/landing_pages/constants.ts b/x-pack/plugins/security_solution/public/landing_pages/constants.ts deleted file mode 100644 index a6b72a5e7db4f..0000000000000 --- a/x-pack/plugins/security_solution/public/landing_pages/constants.ts +++ /dev/null @@ -1,36 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; -import { SecurityPageName } from '../app/types'; - -export interface LandingNavGroup { - label: string; - itemIds: SecurityPageName[]; -} - -export const MANAGE_NAVIGATION_CATEGORIES: LandingNavGroup[] = [ - { - label: i18n.translate('xpack.securitySolution.landing.siemTitle', { - defaultMessage: 'SIEM', - }), - itemIds: [SecurityPageName.rules, SecurityPageName.exceptions], - }, - { - label: i18n.translate('xpack.securitySolution.landing.endpointsTitle', { - defaultMessage: 'ENDPOINTS', - }), - itemIds: [ - SecurityPageName.endpoints, - SecurityPageName.policies, - SecurityPageName.trustedApps, - SecurityPageName.eventFilters, - SecurityPageName.blocklist, - SecurityPageName.hostIsolationExceptions, - ], - }, -]; From 50d715a86287f3ec5fdcddb8bee851d19971f6c7 Mon Sep 17 00:00:00 2001 From: semd Date: Tue, 17 May 2022 14:21:05 +0200 Subject: [PATCH 13/25] temporary increase of plugin size limit --- packages/kbn-optimizer/limits.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 561007cb33b23..441227b2e378d 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -93,7 +93,7 @@ pageLoadAssetSize: expressionShape: 34008 interactiveSetup: 80000 expressionTagcloud: 27505 - securitySolution: 273763 + securitySolution: 289364 # This is temporary: https://github.com/elastic/kibana/pull/132210 update-limits when feature flag removed and deprecated code cleaned customIntegrations: 28810 expressionMetricVis: 23121 expressionHeatmap: 27505 From f00c0ad7ed159d96b1cbea765175990147ff8e6c Mon Sep 17 00:00:00 2001 From: semd Date: Tue, 17 May 2022 16:06:49 +0200 Subject: [PATCH 14/25] swap management links order --- x-pack/plugins/security_solution/public/management/links.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/management/links.ts b/x-pack/plugins/security_solution/public/management/links.ts index 07f9602667a9a..91a4c937f4bdd 100644 --- a/x-pack/plugins/security_solution/public/management/links.ts +++ b/x-pack/plugins/security_solution/public/management/links.ts @@ -174,8 +174,8 @@ export const navigationCategories: NavigationCategories = [ SecurityPageName.policies, SecurityPageName.trustedApps, SecurityPageName.eventFilters, - SecurityPageName.blocklist, SecurityPageName.hostIsolationExceptions, + SecurityPageName.blocklist, ], }, ] as const; From dbd28f647c339874bfff0c4747d6487d560f7723 Mon Sep 17 00:00:00 2001 From: semd Date: Tue, 17 May 2022 16:35:57 +0200 Subject: [PATCH 15/25] improve performance closing nav panel --- .../solution_grouped_nav/solution_grouped_nav.tsx | 13 +++++++------ .../solution_grouped_nav_panel.tsx | 4 +++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.tsx index 70edb7afd37de..5cb7e61c0bab5 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.tsx @@ -63,12 +63,12 @@ export const SolutionGroupedNavComponent: React.FC = ({ setActivePanelNavId(id); }; - const closePanelNav = () => { + const onClosePanelNav = useCallback(() => { activePanelNavIdRef.current = null; setActivePanelNavId(null); - }; + }, []); - const onClosePanelNav = useCallback(() => { + const onOutsidePanelClick = useCallback(() => { const currentPanelNavId = activePanelNavIdRef.current; setTimeout(() => { // This event is triggered on outside click. @@ -76,10 +76,10 @@ export const SolutionGroupedNavComponent: React.FC = ({ // closes also if the active panel button has been clicked (toggle), // but it does not close if any some other panel open button has been clicked. if (activePanelNavIdRef.current === currentPanelNavId) { - closePanelNav(); + onClosePanelNav(); } }); - }, []); + }, [onClosePanelNav]); const navItemsById = useMemo( () => @@ -104,12 +104,13 @@ export const SolutionGroupedNavComponent: React.FC = ({ return ( ); - }, [activePanelNavId, navItemsById, onClosePanelNav]); + }, [activePanelNavId, navItemsById, onClosePanelNav, onOutsidePanelClick]); return ( <> diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_panel.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_panel.tsx index 2cdd2bbdf36fc..b39c878c7ff90 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_panel.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_panel.tsx @@ -30,6 +30,7 @@ import { NavigationCategories } from '../types'; export interface SolutionNavPanelProps { onClose: () => void; + onOutsideClick: () => void; title: string; items: DefaultSideNavItem[]; categories?: NavigationCategories; @@ -53,6 +54,7 @@ export interface SolutionNavPanelItemProps { */ const SolutionNavPanelComponent: React.FC = ({ onClose, + onOutsideClick, title, categories, items, @@ -77,7 +79,7 @@ const SolutionNavPanelComponent: React.FC = ({ - onClose()}> + Date: Tue, 17 May 2022 16:59:56 +0200 Subject: [PATCH 16/25] test updated --- .../solution_grouped_nav_panel.test.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_panel.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_panel.test.tsx index 21a620d9e2e9a..8215d9c0b9f40 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_panel.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_panel.test.tsx @@ -34,11 +34,18 @@ const mockItems: DefaultSideNavItem[] = [ const PANEL_TITLE = 'test title'; const mockOnClose = jest.fn(); +const mockOnOutsideClick = jest.fn(); const renderNavPanel = (props: Partial = {}) => render( <>
- + , { wrapper: TestProviders, @@ -104,7 +111,7 @@ describe('SolutionGroupedNav', () => { const result = renderNavPanel(); result.getByTestId('outsideClickDummy').click(); waitFor(() => { - expect(mockOnClose).toHaveBeenCalled(); + expect(mockOnOutsideClick).toHaveBeenCalled(); }); }); }); From e1fe85b14baf1b61c8e18793dfc33e6b691d155d Mon Sep 17 00:00:00 2001 From: semd Date: Wed, 18 May 2022 14:47:51 +0200 Subject: [PATCH 17/25] host isolation page filterd and some improvements --- .../public/app/deep_links/index.ts | 7 +- .../common/components/navigation/nav_links.ts | 1 + .../security_side_nav.test.tsx | 48 ++-- .../security_side_nav/security_side_nav.tsx | 49 ++-- .../common/components/navigation/types.ts | 9 +- .../public/common/links/app_links.ts | 30 ++- .../public/common/links/index.tsx | 1 - .../public/common/links/links.test.ts | 250 +++++++----------- .../public/common/links/links.ts | 211 +++++++++------ .../public/common/links/types.ts | 17 +- .../landing_pages/pages/manage.test.tsx | 102 +++++-- .../public/landing_pages/pages/manage.tsx | 74 +++--- .../public/management/links.ts | 53 ++-- .../security_solution/public/plugin.tsx | 78 +++--- .../security_solution/server/ui_settings.ts | 2 +- 15 files changed, 514 insertions(+), 418 deletions(-) diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.ts index 76688180a05ba..85ef0629a8769 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.ts @@ -63,7 +63,6 @@ import { } from '../../../common/constants'; import { ExperimentalFeatures } from '../../../common/experimental_features'; import { subscribeAppLinks } from '../../common/links'; -import { getAllAppLinks } from '../../common/links/app_links'; import { AppLinkItems } from '../../common/links/types'; const FEATURE = { @@ -553,9 +552,6 @@ export function isPremiumLicense(licenseType?: LicenseType): boolean { * The code below manages the new implementation using the unified appLinks. */ -// Returns all deep links without filtering, for the initial application register -export const getAllDeepLinks = (): AppDeepLink[] => formatDeepLinks(getAllAppLinks()); - const formatDeepLinks = (appLinks: AppLinkItems): AppDeepLink[] => appLinks.map((appLink) => ({ id: appLink.id, @@ -572,6 +568,9 @@ const formatDeepLinks = (appLinks: AppLinkItems): AppDeepLink[] => : {}), })); +/** + * Registers any change in appLinks to be updated in app deepLinks + */ export const registerDeepLinksUpdater = (appUpdater$: Subject) => { subscribeAppLinks((appLinks) => { appUpdater$.next(() => ({ diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/nav_links.ts b/x-pack/plugins/security_solution/public/common/components/navigation/nav_links.ts index 6d45d0aeed764..db8b5788b04d6 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/nav_links.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/nav_links.ts @@ -25,6 +25,7 @@ const formatNavLinkItems = (appLinks: AppLinkItems): NavLinkItem[] => appLinks.map((link) => ({ id: link.id, title: link.title, + ...(link.categories != null ? { categories: link.categories } : {}), ...(link.description != null ? { description: link.description } : {}), ...(link.sideNavDisabled === true ? { disabled: true } : {}), ...(link.landingIcon != null ? { icon: link.landingIcon } : {}), diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.test.tsx index f63ee1bd5e9c6..846d4ab8a4d64 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.test.tsx @@ -11,22 +11,13 @@ import { SecurityPageName } from '../../../../app/types'; import { TestProviders } from '../../../mock'; import { SecuritySideNav } from './security_side_nav'; import { SolutionGroupedNavProps } from '../solution_grouped_nav/solution_grouped_nav'; -import { navigationCategories as managementCategories } from '../../../../management/links'; import { NavLinkItem } from '../types'; -const mockSolutionGroupedNav = jest.fn((_: SolutionGroupedNavProps) => <>); -jest.mock('../solution_grouped_nav', () => ({ - SolutionGroupedNav: (props: SolutionGroupedNavProps) => mockSolutionGroupedNav(props), -})); -const mockUseRouteSpy = [{ pageName: SecurityPageName.alerts }]; -jest.mock('../../../utils/route/use_route_spy', () => ({ - useRouteSpy: () => mockUseRouteSpy, -})); - -const manageNavLink = { +const manageNavLink: NavLinkItem = { id: SecurityPageName.administration, title: 'manage', description: 'manage description', + categories: [{ label: 'test category', linkIds: [SecurityPageName.endpoints] }], links: [ { id: SecurityPageName.endpoints, @@ -35,17 +26,31 @@ const manageNavLink = { }, ], }; -const alertsNavLink = { +const alertsNavLink: NavLinkItem = { id: SecurityPageName.alerts, title: 'alerts', description: 'alerts description', }; -const mockUseAppNavLinks = jest.fn((): NavLinkItem[] => [alertsNavLink]); +const mockSolutionGroupedNav = jest.fn((_: SolutionGroupedNavProps) => <>); +jest.mock('../solution_grouped_nav', () => ({ + SolutionGroupedNav: (props: SolutionGroupedNavProps) => mockSolutionGroupedNav(props), +})); +const mockUseRouteSpy = jest.fn(() => [{ pageName: SecurityPageName.alerts }]); +jest.mock('../../../utils/route/use_route_spy', () => ({ + useRouteSpy: () => mockUseRouteSpy(), +})); +jest.mock('../../../../management/pages/host_isolation_exceptions/view/hooks', () => ({ + useCanSeeHostIsolationExceptionsMenu: () => true, +})); +jest.mock('../../../links', () => ({ + getAncestorLinksInfo: (id: string) => [{ id }], +})); + +const mockUseAppNavLinks = jest.fn((): NavLinkItem[] => [alertsNavLink, manageNavLink]); jest.mock('../nav_links', () => ({ useAppNavLinks: () => mockUseAppNavLinks(), })); - jest.mock('../../links', () => ({ useGetSecuritySolutionLinkProps: () => @@ -80,6 +85,16 @@ describe('SecuritySideNav', () => { }); }); + it('should render with selected id', () => { + mockUseRouteSpy.mockReturnValueOnce([{ pageName: SecurityPageName.administration }]); + renderNav(); + expect(mockSolutionGroupedNav).toHaveBeenCalledWith( + expect.objectContaining({ + selectedId: SecurityPageName.administration, + }) + ); + }); + it('should render footer items', () => { mockUseAppNavLinks.mockReturnValueOnce([manageNavLink]); renderNav(); @@ -92,7 +107,7 @@ describe('SecuritySideNav', () => { id: SecurityPageName.administration, label: 'manage', href: '/administration', - categories: managementCategories, + categories: manageNavLink.categories, items: [ { id: SecurityPageName.endpoints, @@ -106,6 +121,7 @@ describe('SecuritySideNav', () => { }) ); }); + it('should not render disabled items', () => { mockUseAppNavLinks.mockReturnValueOnce([ { ...alertsNavLink, disabled: true }, @@ -131,7 +147,7 @@ describe('SecuritySideNav', () => { id: SecurityPageName.administration, label: 'manage', href: '/administration', - categories: managementCategories, + categories: manageNavLink.categories, items: [], }, ], diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.tsx index 296901c58c3ad..5ea90887cd685 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.tsx @@ -5,18 +5,18 @@ * 2.0. */ -import React, { useMemo, useCallback, MouseEventHandler } from 'react'; -import { EuiHorizontalRule, EuiLink, EuiListGroupItem } from '@elastic/eui'; +import React, { useMemo, useCallback, useEffect } from 'react'; +import { EuiHorizontalRule, EuiListGroupItem } from '@elastic/eui'; import { SecurityPageName } from '../../../../app/types'; -import { navigationCategories as managementCategories } from '../../../../management/links'; -import { getAncestorLinksInfo } from '../../../links'; +import { excludeAppLink, getAncestorLinksInfo } from '../../../links'; import { useRouteSpy } from '../../../utils/route/use_route_spy'; -import { useGetSecuritySolutionLinkProps } from '../../links'; +import { SecuritySolutionLinkAnchor, useGetSecuritySolutionLinkProps } from '../../links'; import { useAppNavLinks } from '../nav_links'; import { SolutionGroupedNav } from '../solution_grouped_nav'; import { CustomSideNavItem, DefaultSideNavItem, SideNavItem } from '../solution_grouped_nav/types'; import { NavLinkItem } from '../types'; import { EuiIconLaunch } from './icons/launch'; +import { useCanSeeHostIsolationExceptionsMenu } from '../../../../management/pages/host_isolation_exceptions/view/hooks'; const isFooterNavItem = (id: SecurityPageName) => id === SecurityPageName.landing || id === SecurityPageName.administration; @@ -29,11 +29,11 @@ type FormatSideNavItems = (navItems: NavLinkItem) => SideNavItem; const GetStartedCustomLinkComponent: React.FC<{ isSelected: boolean; title: string; - href: string; - onClick: MouseEventHandler; -}> = ({ isSelected, title, href, onClick }) => ( - // eslint-disable-next-line @elastic/eui/href-or-on-click - +}> = ({ isSelected, title }) => ( + - + ); const GetStartedCustomLink = React.memo(GetStartedCustomLinkComponent); @@ -60,6 +60,9 @@ const useFormatSideNavItem = (): FormatSideNavItems => { id: navItem.id, label: navItem.title, ...getSecuritySolutionLinkProps({ deepLinkId: navItem.id }), + ...(navItem.categories && navItem.categories.length > 0 + ? { categories: navItem.categories } + : {}), ...(navItem.links && navItem.links.length > 0 ? { items: navItem.links @@ -74,25 +77,13 @@ const useFormatSideNavItem = (): FormatSideNavItems => { : {}), }); - const formatManagementItem = (navItem: NavLinkItem): DefaultSideNavItem => ({ - ...formatDefaultItem(navItem), - categories: managementCategories, - }); - const formatGetStartedItem = (navItem: NavLinkItem): CustomSideNavItem => ({ id: navItem.id, render: (isSelected) => ( - + ), }); - if (navLinkItem.id === SecurityPageName.administration) { - return formatManagementItem(navLinkItem); - } if (navLinkItem.id === SecurityPageName.landing) { return formatGetStartedItem(navLinkItem); } @@ -132,7 +123,6 @@ const useSideNavItems = () => { const useSelectedId = (): SecurityPageName => { const [{ pageName }] = useRouteSpy(); - const selectedId = useMemo(() => { const [rootLinkInfo] = getAncestorLinksInfo(pageName as SecurityPageName); return rootLinkInfo?.id ?? ''; @@ -148,5 +138,14 @@ const useSelectedId = (): SecurityPageName => { export const SecuritySideNav: React.FC = () => { const [items, footerItems] = useSideNavItems(); const selectedId = useSelectedId(); + + // TODO: move this exclusion logic to getManagementLinkItems + const canSeeHostIsolationExceptions = useCanSeeHostIsolationExceptionsMenu(); + useEffect(() => { + if (!canSeeHostIsolationExceptions) { + excludeAppLink(SecurityPageName.hostIsolationExceptions); + } + }, [canSeeHostIsolationExceptions]); + return ; }; 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 43645cce283a7..85d504165484b 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 @@ -10,6 +10,7 @@ import { UrlStateType } from '../url_state/constants'; import { SecurityPageName } from '../../../app/types'; import { UrlState } from '../url_state/types'; import { SiemRouteType } from '../../utils/route/types'; +import { LinkCategories } from '../../links'; export interface TabNavigationComponentProps { pageName: string; @@ -77,14 +78,8 @@ export type GetUrlForApp = ( ) => string; export type NavigateToUrl = (url: string) => void; - -export interface NavigationCategory { - label: string; - linkIds: readonly SecurityPageName[]; -} - -export type NavigationCategories = Readonly; export interface NavLinkItem { + categories?: LinkCategories; description?: string; disabled?: boolean; icon?: IconType; diff --git a/x-pack/plugins/security_solution/public/common/links/app_links.ts b/x-pack/plugins/security_solution/public/common/links/app_links.ts index c1f1a39a495e3..45a7ed373222f 100644 --- a/x-pack/plugins/security_solution/public/common/links/app_links.ts +++ b/x-pack/plugins/security_solution/public/common/links/app_links.ts @@ -4,22 +4,30 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { CoreStart } from '@kbn/core/public'; import { AppLinkItems } from './types'; import { links as detectionLinks } from '../../detections/links'; import { links as timelinesLinks } from '../../timelines/links'; import { getCasesLinkItems } from '../../cases/links'; -import { links as managementLinks } from '../../management/links'; +import { getManagementLinkItems } from '../../management/links'; import { dashboardsLandingLinks, threatHuntingLandingLinks } from '../../landing_pages/links'; import { gettingStartedLinks } from '../../overview/links'; +import { StartPlugins } from '../../types'; -const appLinks: AppLinkItems = Object.freeze([ - dashboardsLandingLinks, - detectionLinks, - timelinesLinks, - getCasesLinkItems(), - threatHuntingLandingLinks, - gettingStartedLinks, - managementLinks, -]); +export const getAppLinks = async ( + core: CoreStart, + plugins: StartPlugins +): Promise => { + const managementLinks = await getManagementLinkItems(core, plugins); + const casesLinks = getCasesLinkItems(); -export const getAllAppLinks = () => appLinks; + return Object.freeze([ + dashboardsLandingLinks, + detectionLinks, + timelinesLinks, + casesLinks, + threatHuntingLandingLinks, + gettingStartedLinks, + managementLinks, + ]); +}; diff --git a/x-pack/plugins/security_solution/public/common/links/index.tsx b/x-pack/plugins/security_solution/public/common/links/index.tsx index d9565a8edb98d..e4e4de0b49430 100644 --- a/x-pack/plugins/security_solution/public/common/links/index.tsx +++ b/x-pack/plugins/security_solution/public/common/links/index.tsx @@ -6,5 +6,4 @@ */ export * from './links'; -export * from './app_links'; export * from './types'; diff --git a/x-pack/plugins/security_solution/public/common/links/links.test.ts b/x-pack/plugins/security_solution/public/common/links/links.test.ts index 4524167b3b2c0..896f9357077c8 100644 --- a/x-pack/plugins/security_solution/public/common/links/links.test.ts +++ b/x-pack/plugins/security_solution/public/common/links/links.test.ts @@ -16,33 +16,30 @@ import { getAncestorLinksInfo, getLinkInfo, needsUrlState, - updateAllAppLinks, updateAppLinks, + excludeAppLink, } from './links'; -jest.mock('./app_links', () => ({ - getAllAppLinks: () => [ - { - id: 'hosts', - title: 'Hosts', - path: '/hosts', - links: [ - { - id: 'hosts-authentications', - title: 'Authentications', - path: `/hosts/authentications`, - experimentalKey: 'nonExistingKey', - }, - { - id: 'hosts-events', - title: 'Events', - path: `/hosts/events`, - skipUrlState: true, - }, - ], - }, - ], -})); +const defaultAppLinks: AppLinkItems = [ + { + id: SecurityPageName.hosts, + title: 'Hosts', + path: '/hosts', + links: [ + { + id: SecurityPageName.hostsAuthentications, + title: 'Authentications', + path: `/hosts/authentications`, + }, + { + id: SecurityPageName.hostsEvents, + title: 'Events', + path: `/hosts/events`, + skipUrlState: true, + }, + ], + }, +]; const mockExperimentalDefaults = mockGlobalState.app.enableExperimental; @@ -63,111 +60,39 @@ const renderUseAppLinks = () => describe('Security app links', () => { beforeEach(() => { mockLicense.hasAtLeast = licensePremiumMock; + + updateAppLinks(defaultAppLinks, { + capabilities: mockCapabilities, + experimentalFeatures: mockExperimentalDefaults, + license: mockLicense, + }); }); describe('useAppLinks', () => { it('should return initial appLinks', () => { const { result } = renderUseAppLinks(); - expect(result.current).toMatchInlineSnapshot(` - Array [ - Object { - "id": "hosts", - "links": Array [ - Object { - "experimentalKey": "nonExistingKey", - "id": "hosts-authentications", - "path": "/hosts/authentications", - "title": "Authentications", - }, - Object { - "id": "hosts-events", - "path": "/hosts/events", - "skipUrlState": true, - "title": "Events", - }, - ], - "path": "/hosts", - "title": "Hosts", - }, - ] - `); - }); - - it('should update all appLinks with filtering', async () => { - const { result, waitForNextUpdate } = renderUseAppLinks(); - await act(async () => { - updateAllAppLinks({ - capabilities: mockCapabilities, - experimentalFeatures: mockExperimentalDefaults, - license: mockLicense, - }); - await waitForNextUpdate(); - }); - expect(result.current).toMatchInlineSnapshot(` - Array [ - Object { - "id": "hosts", - "links": Array [ - Object { - "id": "hosts-events", - "path": "/hosts/events", - "skipUrlState": true, - "title": "Events", - }, - ], - "path": "/hosts", - "title": "Hosts", - }, - ] - `); - }); - - it('should manually update appLinks with filtering', async () => { - const { result, waitForNextUpdate } = renderUseAppLinks(); - await act(async () => { - updateAppLinks( - [ - { - id: SecurityPageName.network, - title: 'Network', - path: '/network', - }, - ], - { - capabilities: mockCapabilities, - experimentalFeatures: mockExperimentalDefaults, - license: mockLicense, - } - ); - await waitForNextUpdate(); - }); - expect(result.current).toMatchInlineSnapshot(` - Array [ - Object { - "id": "network", - "path": "/network", - "title": "Network", - }, - ] - `); + expect(result.current).toStrictEqual(defaultAppLinks); }); it('should filter not allowed links', async () => { const { result, waitForNextUpdate } = renderUseAppLinks(); + // this link should not be excluded, the test checks all conditions are passed + const networkLinkItem = { + id: SecurityPageName.network, + title: 'Network', + path: '/network', + capabilities: [`${CASES_FEATURE_ID}.read_cases`, `${SERVER_APP_ID}.show`], + experimentalKey: 'flagEnabled' as unknown as keyof typeof mockExperimentalDefaults, + hideWhenExperimentalKey: 'flagDisabled' as unknown as keyof typeof mockExperimentalDefaults, + licenseType: 'basic' as const, + }; + await act(async () => { updateAppLinks( [ { - // this link should not be excluded, the test checks all conditions passed - // all its sub-links should be filtered for each criteria - id: SecurityPageName.network, - title: 'Network', - path: '/network', - capabilities: [`${CASES_FEATURE_ID}.read_cases`, `${SERVER_APP_ID}.show`], - experimentalKey: 'flagEnabled' as unknown as keyof typeof mockExperimentalDefaults, - hideWhenExperimentalKey: - 'flagDisabled' as unknown as keyof typeof mockExperimentalDefaults, - licenseType: 'basic', + ...networkLinkItem, + // all its links should be filtered for all different criteria links: [ { id: SecurityPageName.networkExternalAlerts, @@ -201,6 +126,7 @@ describe('Security app links', () => { ], }, { + // should be excluded by license with all its links id: SecurityPageName.hosts, title: 'Hosts', path: '/hosts', @@ -228,68 +154,74 @@ describe('Security app links', () => { ); await waitForNextUpdate(); }); - expect(result.current).toMatchInlineSnapshot(` - Array [ - Object { - "capabilities": Array [ - "securitySolutionCases.read_cases", - "siem.show", - ], - "experimentalKey": "flagEnabled", - "hideWhenExperimentalKey": "flagDisabled", - "id": "network", - "licenseType": "basic", - "path": "/network", - "title": "Network", - }, - ] - `); + + expect(result.current).toStrictEqual([networkLinkItem]); + }); + }); + + describe('excludeAppLink', () => { + it('should exclude link from app links', async () => { + const { result, waitForNextUpdate } = renderUseAppLinks(); + await act(async () => { + excludeAppLink(SecurityPageName.hostsEvents); + await waitForNextUpdate(); + }); + expect(result.current).toStrictEqual([ + { + id: SecurityPageName.hosts, + title: 'Hosts', + path: '/hosts', + links: [ + { + id: SecurityPageName.hostsAuthentications, + title: 'Authentications', + path: `/hosts/authentications`, + }, + ], + }, + ]); }); }); describe('getAncestorLinksInfo', () => { - it('finds ancestors flattened links', () => { + it('should find ancestors flattened links', () => { const hierarchy = getAncestorLinksInfo(SecurityPageName.hostsEvents); - expect(hierarchy).toMatchInlineSnapshot(` - Array [ - Object { - "id": "hosts", - "path": "/hosts", - "title": "Hosts", - }, - Object { - "id": "hosts-events", - "path": "/hosts/events", - "skipUrlState": true, - "title": "Events", - }, - ] - `); + expect(hierarchy).toStrictEqual([ + { + id: SecurityPageName.hosts, + path: '/hosts', + title: 'Hosts', + }, + { + id: SecurityPageName.hostsEvents, + path: '/hosts/events', + skipUrlState: true, + title: 'Events', + }, + ]); }); }); describe('needsUrlState', () => { - it('returns true when url state exists for page', () => { + it('should return true when url state exists for page', () => { const needsUrl = needsUrlState(SecurityPageName.hosts); expect(needsUrl).toEqual(true); }); - it('returns false when url state does not exist for page', () => { + it('should return false when url state does not exist for page', () => { const needsUrl = needsUrlState(SecurityPageName.hostsEvents); expect(needsUrl).toEqual(false); }); }); describe('getLinkInfo', () => { - it('gets information for an individual link', () => { + it('should get information for an individual link', () => { const linkInfo = getLinkInfo(SecurityPageName.hostsEvents); - expect(linkInfo).toMatchInlineSnapshot(` - Object { - "id": "hosts-events", - "path": "/hosts/events", - "skipUrlState": true, - "title": "Events", - } - `); + expect(linkInfo).toStrictEqual({ + id: SecurityPageName.hostsEvents, + path: '/hosts/events', + skipUrlState: true, + title: 'Events', + }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/links/links.ts b/x-pack/plugins/security_solution/public/common/links/links.ts index a2d06aca90b89..54f224a6b988d 100644 --- a/x-pack/plugins/security_solution/public/common/links/links.ts +++ b/x-pack/plugins/security_solution/public/common/links/links.ts @@ -5,13 +5,12 @@ * 2.0. */ -import { Capabilities } from '@kbn/core/public'; +import type { Capabilities } from '@kbn/core/public'; import { get } from 'lodash'; import { useEffect, useState } from 'react'; import { BehaviorSubject } from 'rxjs'; import { SecurityPageName } from '../../../common/constants'; -import { getAllAppLinks } from './app_links'; -import { +import type { AppLinkItems, LinkInfo, LinkItem, @@ -22,13 +21,31 @@ import { /** * App links updater, it keeps the value of the app links in sync with all application. - * It can be updated using `updateAppLinks` or `updateAllAppLinks`. + * It can be updated using `updateAppLinks` or `excludeAppLink` * Read it using `subscribeAppLinks` or `useAppLinks` hook. */ -const appLinksUpdater$ = new BehaviorSubject(getAllAppLinks()); +const appLinksUpdater$ = new BehaviorSubject<{ + links: AppLinkItems; + normalizedLinks: NormalizedLinks; +}>({ + links: [], // stores the appLinkItems recursive hierarchy + normalizedLinks: {}, // stores a flatten normalized object for direct id access +}); +const getAppLinksValue = () => appLinksUpdater$.getValue().links; +const getNormalizedLinksValue = () => appLinksUpdater$.getValue().normalizedLinks; + +/** + * Subscribes to the updater to get the app links updates + */ +export const subscribeAppLinks = (onChange: (links: AppLinkItems) => void) => + appLinksUpdater$.subscribe(({ links }) => onChange(links)); + +/** + * Hook to get the app links updated value + */ export const useAppLinks = (): AppLinkItems => { - const [appLinks, setAppLinks] = useState(appLinksUpdater$.getValue()); + const [appLinks, setAppLinks] = useState(getAppLinksValue); useEffect(() => { const linksSubscription = subscribeAppLinks((newAppLinks) => { @@ -40,20 +57,122 @@ export const useAppLinks = (): AppLinkItems => { return appLinks; }; -export const subscribeAppLinks = (onChange: (appItems: AppLinkItems) => void) => - appLinksUpdater$.subscribe(onChange); - +/** + * Updates the app links applying the filter by permissions + */ export const updateAppLinks = ( appLinksToUpdate: AppLinkItems, linksPermissions: LinksPermissions ) => { - appLinksUpdater$.next(Object.freeze(getFilteredAppLinks(appLinksToUpdate, linksPermissions))); + const filteredAppLinks = getFilteredAppLinks(appLinksToUpdate, linksPermissions); + appLinksUpdater$.next({ + links: Object.freeze(filteredAppLinks), + normalizedLinks: Object.freeze(getNormalizedLinks(filteredAppLinks)), + }); +}; + +/** + * Excludes a link by id from the current app links + * @deprecated this function will not be needed when async link filtering is migrated to the main getAppLinks functions + */ +export const excludeAppLink = (linkId: SecurityPageName) => { + const { links, normalizedLinks } = appLinksUpdater$.getValue(); + if (!normalizedLinks[linkId]) { + return; + } + + let found = false; + const excludeRec = (currentLinks: AppLinkItems): LinkItem[] => + currentLinks.reduce((acc, link) => { + if (!found) { + if (link.id === linkId) { + found = true; + return acc; + } + if (link.links) { + const excludedLinks = excludeRec(link.links); + if (excludedLinks.length > 0) { + acc.push({ ...link, links: excludedLinks }); + return acc; + } + } + } + acc.push(link); + return acc; + }, []); + + const excludedLinks = excludeRec(links); + + appLinksUpdater$.next({ + links: Object.freeze(excludedLinks), + normalizedLinks: Object.freeze(getNormalizedLinks(excludedLinks)), + }); +}; + +/** + * Returns the `LinkInfo` from a link id parameter + */ +export const getLinkInfo = (id: SecurityPageName): LinkInfo | undefined => { + const normalizedLink = getNormalizedLink(id); + if (!normalizedLink) { + return undefined; + } + // discards the parentId and creates the linkInfo copy. + const { parentId, ...linkInfo } = normalizedLink; + return linkInfo; +}; + +/** + * Returns the `LinkInfo` of all the ancestors to the parameter id link, also included. + */ +export const getAncestorLinksInfo = (id: SecurityPageName): LinkInfo[] => { + const ancestors: LinkInfo[] = []; + let currentId: SecurityPageName | undefined = id; + while (currentId) { + const normalizedLink = getNormalizedLink(currentId); + if (normalizedLink) { + const { parentId, ...linkInfo } = normalizedLink; + ancestors.push(linkInfo); + currentId = parentId; + } else { + currentId = undefined; + } + } + return ancestors.reverse(); +}; + +/** + * Returns `true` if the links needs to carry the application state in the url. + * Defaults to `true` if the `skipUrlState` property of the `LinkItem` is `undefined`. + */ +export const needsUrlState = (id: SecurityPageName): boolean => { + return !getNormalizedLink(id)?.skipUrlState; }; -export const updateAllAppLinks = (linksPermissions: LinksPermissions) => { - updateAppLinks(getAllAppLinks(), linksPermissions); +// Internal functions + +/** + * Creates the `NormalizedLinks` structure from a `LinkItem` array + */ +const getNormalizedLinks = ( + currentLinks: AppLinkItems, + parentId?: SecurityPageName +): NormalizedLinks => { + return currentLinks.reduce((normalized, { links, ...currentLink }) => { + normalized[currentLink.id] = { + ...currentLink, + parentId, + }; + if (links && links.length > 0) { + Object.assign(normalized, getNormalizedLinks(links, currentLink.id)); + } + return normalized; + }, {}); }; +const getNormalizedLink = (id: SecurityPageName): Readonly | undefined => + getNormalizedLinksValue()[id]; + const getFilteredAppLinks = ( appLinkToFilter: AppLinkItems, linksPermissions: LinksPermissions @@ -102,71 +221,3 @@ const isLinkAllowed = ( } return true; }; - -/** - * Recursive function to create the `NormalizedLinks` structure from a `LinkItem` array parameter - */ -const getNormalizedLinks = ( - currentLinks: Readonly, - parentId?: SecurityPageName -): NormalizedLinks => { - const result = currentLinks.reduce>( - (normalized, { links, ...currentLink }) => { - normalized[currentLink.id] = { - ...currentLink, - parentId, - }; - if (links && links.length > 0) { - Object.assign(normalized, getNormalizedLinks(links, currentLink.id)); - } - return normalized; - }, - {} - ); - return result as NormalizedLinks; -}; - -/** - * Normalized indexed version of the global `links` array, referencing the parent by id, instead of having nested links children - */ -const normalizedLinks: Readonly = Object.freeze( - getNormalizedLinks(getAllAppLinks()) -); - -/** - * Returns the `NormalizedLink` from a link id parameter. - * The object reference is frozen to make sure it is not mutated by the caller. - */ -const getNormalizedLink = (id: SecurityPageName): Readonly => - Object.freeze(normalizedLinks[id]); - -/** - * Returns the `LinkInfo` from a link id parameter - */ -export const getLinkInfo = (id: SecurityPageName): LinkInfo => { - // discards the parentId and creates the linkInfo copy. - const { parentId, ...linkInfo } = getNormalizedLink(id); - return linkInfo; -}; - -/** - * Returns the `LinkInfo` of all the ancestors to the parameter id link, also included. - */ -export const getAncestorLinksInfo = (id: SecurityPageName): LinkInfo[] => { - const ancestors: LinkInfo[] = []; - let currentId: SecurityPageName | undefined = id; - while (currentId) { - const { parentId, ...linkInfo } = getNormalizedLink(currentId); - ancestors.push(linkInfo); - currentId = parentId; - } - return ancestors.reverse(); -}; - -/** - * Returns `true` if the links needs to carry the application state in the url. - * Defaults to `true` if the `skipUrlState` property of the `LinkItem` is `undefined`. - */ -export const needsUrlState = (id: SecurityPageName): boolean => { - return !getNormalizedLink(id).skipUrlState; -}; diff --git a/x-pack/plugins/security_solution/public/common/links/types.ts b/x-pack/plugins/security_solution/public/common/links/types.ts index 3ced4c1496582..82d3b20720d92 100644 --- a/x-pack/plugins/security_solution/public/common/links/types.ts +++ b/x-pack/plugins/security_solution/public/common/links/types.ts @@ -20,6 +20,13 @@ export interface LinksPermissions { license?: ILicense; } +export interface LinkCategory { + label: string; + linkIds: readonly SecurityPageName[]; +} + +export type LinkCategories = Readonly; + export interface LinkItem { /** * The description of the link content @@ -34,6 +41,10 @@ export interface LinkItem { * Uses "or" conditional, only one enabled capability is needed to activate the link */ capabilities?: string[]; + /** + * Categories to display in the navigation + */ + categories?: LinkCategories; /** * Enables link in the global navigation. Defaults to false. */ @@ -89,9 +100,9 @@ export interface LinkItem { */ sideNavDisabled?: boolean; /** - * Enables link in the side navigation. Defaults to false. + * Disabled the state query string in the URL. Defaults to false. */ - skipUrlState?: boolean; // defaults to false + skipUrlState?: boolean; /** * Title of the link */ @@ -102,4 +113,4 @@ export type AppLinkItems = Readonly; export type LinkInfo = Omit; export type NormalizedLink = LinkInfo & { parentId?: SecurityPageName }; -export type NormalizedLinks = Record; +export type NormalizedLinks = Partial>; diff --git a/x-pack/plugins/security_solution/public/landing_pages/pages/manage.test.tsx b/x-pack/plugins/security_solution/public/landing_pages/pages/manage.test.tsx index 180517f93605e..06389105cec2b 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/pages/manage.test.tsx +++ b/x-pack/plugins/security_solution/public/landing_pages/pages/manage.test.tsx @@ -9,15 +9,27 @@ import { render } from '@testing-library/react'; import React from 'react'; import { SecurityPageName } from '../../app/types'; import { TestProviders } from '../../common/mock'; -import { LandingCategories } from './manage'; +import { ManagementCategories } from './manage'; import { NavLinkItem } from '../../common/components/navigation/types'; const RULES_ITEM_LABEL = 'elastic rules!'; const EXCEPTIONS_ITEM_LABEL = 'exceptional!'; +const CATEGORY_1_LABEL = 'first tests category'; +const CATEGORY_2_LABEL = 'second tests category'; -const mockAppManageLink: NavLinkItem = { +const defaultAppManageLink: NavLinkItem = { id: SecurityPageName.administration, title: 'admin', + categories: [ + { + label: CATEGORY_1_LABEL, + linkIds: [SecurityPageName.rules], + }, + { + label: CATEGORY_2_LABEL, + linkIds: [SecurityPageName.exceptions], + }, + ], links: [ { id: SecurityPageName.rules, @@ -33,26 +45,17 @@ const mockAppManageLink: NavLinkItem = { }, ], }; + +const mockAppManageLink = jest.fn(() => defaultAppManageLink); jest.mock('../../common/components/navigation/nav_links', () => ({ - useAppRootNavLink: jest.fn(() => mockAppManageLink), + useAppRootNavLink: () => mockAppManageLink(), })); -describe('LandingCategories', () => { - it('renders items', () => { +describe('ManagementCategories', () => { + it('should render items', () => { const { queryByText } = render( - + ); @@ -60,17 +63,19 @@ describe('LandingCategories', () => { expect(queryByText(EXCEPTIONS_ITEM_LABEL)).toBeInTheDocument(); }); - it('renders items in the same order as defined', () => { + it('should render items in the same order as defined', () => { + mockAppManageLink.mockReturnValueOnce({ + ...defaultAppManageLink, + categories: [ + { + label: '', + linkIds: [SecurityPageName.exceptions, SecurityPageName.rules], + }, + ], + }); const { queryAllByTestId } = render( - + ); @@ -79,4 +84,49 @@ describe('LandingCategories', () => { expect(renderedItems[0]).toHaveTextContent(EXCEPTIONS_ITEM_LABEL); expect(renderedItems[1]).toHaveTextContent(RULES_ITEM_LABEL); }); + + it('should not render category items filtered', () => { + mockAppManageLink.mockReturnValueOnce({ + ...defaultAppManageLink, + categories: [ + { + label: CATEGORY_1_LABEL, + linkIds: [SecurityPageName.rules, SecurityPageName.exceptions], + }, + ], + links: [ + { + id: SecurityPageName.rules, + title: RULES_ITEM_LABEL, + description: '', + icon: 'testIcon1', + }, + ], + }); + const { queryAllByTestId } = render( + + + + ); + + const renderedItems = queryAllByTestId('LandingItem'); + + expect(renderedItems).toHaveLength(1); + expect(renderedItems[0]).toHaveTextContent(RULES_ITEM_LABEL); + }); + + it('should not render category if all items filtered', () => { + mockAppManageLink.mockReturnValueOnce({ + ...defaultAppManageLink, + links: [], + }); + const { queryByText } = render( + + + + ); + + expect(queryByText(CATEGORY_1_LABEL)).not.toBeInTheDocument(); + expect(queryByText(CATEGORY_2_LABEL)).not.toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/security_solution/public/landing_pages/pages/manage.tsx b/x-pack/plugins/security_solution/public/landing_pages/pages/manage.tsx index 29e2c2715e98e..06bd0c6258b57 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/pages/manage.tsx +++ b/x-pack/plugins/security_solution/public/landing_pages/pages/manage.tsx @@ -11,17 +11,16 @@ import styled from 'styled-components'; import { SecurityPageName } from '../../app/types'; import { HeaderPage } from '../../common/components/header_page'; import { useAppRootNavLink } from '../../common/components/navigation/nav_links'; -import { NavigationCategories } from '../../common/components/navigation/types'; +import { NavLinkItem } from '../../common/components/navigation/types'; import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; import { SpyRoute } from '../../common/utils/route/spy_routes'; -import { navigationCategories } from '../../management/links'; import { LandingLinksIcons } from '../components/landing_links_icons'; import { MANAGE_PAGE_TITLE } from './translations'; export const ManageLandingPage = () => ( - + ); @@ -31,37 +30,48 @@ const StyledEuiHorizontalRule = styled(EuiHorizontalRule)` margin-bottom: ${({ theme }) => theme.eui.paddingSizes.l}; `; -const useGetManageNavLinks = () => { - const manageNavLinks = useAppRootNavLink(SecurityPageName.administration)?.links ?? []; +type ManagementCategories = Array<{ label: string; links: NavLinkItem[] }>; +const useManagementCategories = (): ManagementCategories => { + const { links = [], categories = [] } = useAppRootNavLink(SecurityPageName.administration) ?? {}; - const manageLinksById = Object.fromEntries(manageNavLinks.map((link) => [link.id, link])); - return (linkIds: readonly SecurityPageName[]) => linkIds.map((linkId) => manageLinksById[linkId]); + const manageLinksById = Object.fromEntries(links.map((link) => [link.id, link])); + + return categories.reduce((acc, { label, linkIds }) => { + const linksItem = linkIds.reduce((linksAcc, linkId) => { + if (manageLinksById[linkId]) { + linksAcc.push(manageLinksById[linkId]); + } + return linksAcc; + }, []); + if (linksItem.length > 0) { + acc.push({ label, links: linksItem }); + } + return acc; + }, []); }; -export const LandingCategories = React.memo( - ({ categories }: { categories: NavigationCategories }) => { - const getManageNavLinks = useGetManageNavLinks(); +export const ManagementCategories = () => { + const managementCategories = useManagementCategories(); - return ( - <> - {categories.map(({ label, linkIds }, index) => ( -
- {index > 0 && ( - <> - - - - )} - -

{label}

-
- - -
- ))} - - ); - } -); + return ( + <> + {managementCategories.map(({ label, links }, index) => ( +
+ {index > 0 && ( + <> + + + + )} + +

{label}

+
+ + +
+ ))} + + ); +}; -LandingCategories.displayName = 'LandingCategories'; +ManagementCategories.displayName = 'ManagementCategories'; diff --git a/x-pack/plugins/security_solution/public/management/links.ts b/x-pack/plugins/security_solution/public/management/links.ts index 91a4c937f4bdd..8cebc159b2ccb 100644 --- a/x-pack/plugins/security_solution/public/management/links.ts +++ b/x-pack/plugins/security_solution/public/management/links.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { CoreStart } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; import { BLOCKLIST_PATH, @@ -30,8 +31,8 @@ import { RULES, TRUSTED_APPLICATIONS, } from '../app/translations'; -import { NavigationCategories } from '../common/components/navigation/types'; import { LinkItem } from '../common/links/types'; +import { StartPlugins } from '../types'; import { IconBlocklist } from './icons/blocklist'; import { IconEndpoints } from './icons/endpoints'; @@ -42,7 +43,29 @@ import { IconHostIsolation } from './icons/host_isolation'; import { IconSiemRules } from './icons/siem_rules'; import { IconTrustedApplications } from './icons/trusted_applications'; -export const links: LinkItem = { +const categories = [ + { + label: i18n.translate('xpack.securitySolution.appLinks.category.siem', { + defaultMessage: 'SIEM', + }), + linkIds: [SecurityPageName.rules, SecurityPageName.exceptions], + }, + { + label: i18n.translate('xpack.securitySolution.appLinks.category.endpoints', { + defaultMessage: 'ENDPOINTS', + }), + linkIds: [ + SecurityPageName.endpoints, + SecurityPageName.policies, + SecurityPageName.trustedApps, + SecurityPageName.eventFilters, + SecurityPageName.hostIsolationExceptions, + SecurityPageName.blocklist, + ], + }, +]; + +const links: LinkItem = { id: SecurityPageName.administration, title: MANAGE, path: MANAGE_PATH, @@ -54,6 +77,7 @@ export const links: LinkItem = { defaultMessage: 'Manage', }), ], + categories, links: [ { id: SecurityPageName.rules, @@ -158,24 +182,7 @@ export const links: LinkItem = { ], }; -export const navigationCategories: NavigationCategories = [ - { - label: i18n.translate('xpack.securitySolution.appLinks.category.siem', { - defaultMessage: 'SIEM', - }), - linkIds: [SecurityPageName.rules, SecurityPageName.exceptions], - }, - { - label: i18n.translate('xpack.securitySolution.appLinks.category.endpoints', { - defaultMessage: 'ENDPOINTS', - }), - linkIds: [ - SecurityPageName.endpoints, - SecurityPageName.policies, - SecurityPageName.trustedApps, - SecurityPageName.eventFilters, - SecurityPageName.hostIsolationExceptions, - SecurityPageName.blocklist, - ], - }, -] as const; +export const getManagementLinkItems = async (core: CoreStart, plugins: StartPlugins) => { + // TODO: implement async logic to exclude links + return links; +}; diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 9934fb13e11b2..942067454e1fb 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -45,9 +45,11 @@ import { DETECTION_ENGINE_INDEX_URL, SERVER_APP_ID, SOURCERER_API_URL, + ENABLE_GROUPED_NAVIGATION, } from '../common/constants'; -import { getDeepLinks } from './app/deep_links'; +import { getDeepLinks, registerDeepLinksUpdater } from './app/deep_links'; +import { updateAppLinks } from './common/links'; import { getSubPluginRoutesByCapabilities, manageOldSiemRoutes } from './helpers'; import { SecurityAppStore } from './common/store/store'; import { licenseService } from './common/hooks/use_license'; @@ -64,7 +66,6 @@ import { import { LazyEndpointCustomAssetsExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_custom_assets_extension'; import { initDataView, SourcererModel, KibanaDataView } from './common/store/sourcerer/model'; import { SecurityDataView } from './common/containers/sourcerer/api'; -import { updateAllAppLinks } from './common/links'; export class Plugin implements IPlugin { readonly kibanaVersion: string; @@ -141,7 +142,6 @@ export class Plugin implements IPlugin { // required to show the alert table inside cases const { alertsTableConfigurationRegistry } = plugins.triggersActionsUi; @@ -191,7 +191,7 @@ export class Plugin implements IPlugin { if (currentLicense.type !== undefined) { - updateAllAppLinks({ + updateAppLinks(appLinks, { experimentalFeatures: this.experimentalFeatures, license: currentLicense, capabilities: core.application.capabilities, }); - // TODO: to be removed - this.appUpdater$.next(() => ({ - navLinkStatus: AppNavLinkStatus.hidden, // workaround to prevent main navLink to switch to visible after update. should not be needed - deepLinks: getDeepLinks( - this.experimentalFeatures, - currentLicense.type, - core.application.capabilities - ), - })); + if (!newNavEnabled) { + // TODO: remove block when nav flag no longer needed + this.appUpdater$.next(() => ({ + navLinkStatus: AppNavLinkStatus.hidden, // workaround to prevent main navLink to switch to visible after update. should not be needed + deepLinks: getDeepLinks( + this.experimentalFeatures, + currentLicense.type, + core.application.capabilities + ), + })); + } } }); } else { - updateAllAppLinks({ + updateAppLinks(appLinks, { experimentalFeatures: this.experimentalFeatures, capabilities: core.application.capabilities, }); - // TODO: to be removed - this.appUpdater$.next(() => ({ - navLinkStatus: AppNavLinkStatus.hidden, // workaround to prevent main navLink to switch to visible after update. should not be needed - deepLinks: getDeepLinks( - this.experimentalFeatures, - undefined, - core.application.capabilities - ), - })); + if (!newNavEnabled) { + // TODO: remove block when nav flag no longer needed + this.appUpdater$.next(() => ({ + navLinkStatus: AppNavLinkStatus.hidden, // workaround to prevent main navLink to switch to visible after update. should not be needed + deepLinks: getDeepLinks( + this.experimentalFeatures, + undefined, + core.application.capabilities + ), + })); + } } return {}; @@ -317,11 +324,22 @@ export class Plugin implements IPlugin Date: Wed, 18 May 2022 15:40:01 +0200 Subject: [PATCH 18/25] remove async from plugin start --- .../security_side_nav.test.tsx | 7 +++ .../security_side_nav/security_side_nav.tsx | 6 +- .../solution_grouped_nav.tsx | 6 +- .../solution_grouped_nav_panel.tsx | 6 +- .../navigation/solution_grouped_nav/types.ts | 4 +- .../public/common/links/types.ts | 2 +- .../security_solution/public/plugin.tsx | 56 ++++++++++--------- 7 files changed, 50 insertions(+), 37 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.test.tsx index 846d4ab8a4d64..da6cfd57ba109 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.test.tsx @@ -85,6 +85,13 @@ describe('SecuritySideNav', () => { }); }); + it('should render the loader if items are still empty', () => { + mockUseAppNavLinks.mockReturnValueOnce([]); + const result = renderNav(); + expect(result.getByTestId('sideNavLoader')).toBeInTheDocument(); + expect(mockSolutionGroupedNav).not.toHaveBeenCalled(); + }); + it('should render with selected id', () => { mockUseRouteSpy.mockReturnValueOnce([{ pageName: SecurityPageName.administration }]); renderNav(); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.tsx index 5ea90887cd685..139c9fa4d1acb 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.tsx @@ -6,7 +6,7 @@ */ import React, { useMemo, useCallback, useEffect } from 'react'; -import { EuiHorizontalRule, EuiListGroupItem } from '@elastic/eui'; +import { EuiHorizontalRule, EuiListGroupItem, EuiLoadingSpinner } from '@elastic/eui'; import { SecurityPageName } from '../../../../app/types'; import { excludeAppLink, getAncestorLinksInfo } from '../../../links'; import { useRouteSpy } from '../../../utils/route/use_route_spy'; @@ -147,5 +147,9 @@ export const SecuritySideNav: React.FC = () => { } }, [canSeeHostIsolationExceptions]); + if (items.length === 0 && footerItems.length === 0) { + return ; + } + return ; }; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.tsx index 5cb7e61c0bab5..073723b80f518 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav.tsx @@ -19,7 +19,7 @@ import { SolutionNavPanel } from './solution_grouped_nav_panel'; import { EuiListGroupItemStyled } from './solution_grouped_nav.styles'; import { DefaultSideNavItem, SideNavItem, isCustomItem, isDefaultItem } from './types'; import { EuiIconSpaces } from './icons/spaces'; -import { NavigationCategories } from '../types'; +import type { LinkCategories } from '../../../links'; export interface SolutionGroupedNavProps { items: SideNavItem[]; @@ -45,7 +45,7 @@ export interface SolutionNavItemProps { type ActivePanelNav = string | null; type NavItemsById = Record< string, - { title: string; panelItems: DefaultSideNavItem[]; categories?: NavigationCategories } + { title: string; panelItems: DefaultSideNavItem[]; categories?: LinkCategories } >; export const SolutionGroupedNavComponent: React.FC = ({ @@ -74,7 +74,7 @@ export const SolutionGroupedNavComponent: React.FC = ({ // This event is triggered on outside click. // Closing the side nav at the end of event loop to make sure it // closes also if the active panel button has been clicked (toggle), - // but it does not close if any some other panel open button has been clicked. + // but it does not close if any any other panel open button has been clicked. if (activePanelNavIdRef.current === currentPanelNavId) { onClosePanelNav(); } diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_panel.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_panel.tsx index b39c878c7ff90..a418f666d2782 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_panel.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/solution_grouped_nav_panel.tsx @@ -26,17 +26,17 @@ import classNames from 'classnames'; import { EuiPanelStyled } from './solution_grouped_nav_panel.styles'; import { useShowTimeline } from '../../../utils/timeline/use_show_timeline'; import type { DefaultSideNavItem } from './types'; -import { NavigationCategories } from '../types'; +import type { LinkCategories } from '../../../links/types'; export interface SolutionNavPanelProps { onClose: () => void; onOutsideClick: () => void; title: string; items: DefaultSideNavItem[]; - categories?: NavigationCategories; + categories?: LinkCategories; } export interface SolutionNavPanelCategoriesProps { - categories: NavigationCategories; + categories: LinkCategories; items: DefaultSideNavItem[]; onClose: () => void; } diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/types.ts b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/types.ts index 3644c25d0493d..a16bad9126d09 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/solution_grouped_nav/types.ts @@ -6,8 +6,8 @@ */ import React from 'react'; -import { NavigationCategories } from '../types'; import type { SecurityPageName } from '../../../../app/types'; +import type { LinkCategories } from '../../../links/types'; export interface DefaultSideNavItem { id: SecurityPageName; @@ -16,7 +16,7 @@ export interface DefaultSideNavItem { onClick?: React.MouseEventHandler; description?: string; items?: DefaultSideNavItem[]; - categories?: NavigationCategories; + categories?: LinkCategories; } export interface CustomSideNavItem { diff --git a/x-pack/plugins/security_solution/public/common/links/types.ts b/x-pack/plugins/security_solution/public/common/links/types.ts index 82d3b20720d92..8e858ffdaa95e 100644 --- a/x-pack/plugins/security_solution/public/common/links/types.ts +++ b/x-pack/plugins/security_solution/public/common/links/types.ts @@ -100,7 +100,7 @@ export interface LinkItem { */ sideNavDisabled?: boolean; /** - * Disabled the state query string in the URL. Defaults to false. + * Disables the state query string in the URL. Defaults to false. */ skipUrlState?: boolean; /** diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 942067454e1fb..a82c8c68ffe7e 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -191,7 +191,7 @@ export class Plugin implements IPlugin { - if (currentLicense.type !== undefined) { + // Not using await to prevent blocking start execution + this.lazyApplicationLinks().then(({ getAppLinks }) => { + getAppLinks(core, plugins).then((appLinks) => { + if (licensing !== null) { + this.licensingSubscription = licensing.subscribe((currentLicense) => { + if (currentLicense.type !== undefined) { + updateAppLinks(appLinks, { + experimentalFeatures: this.experimentalFeatures, + license: currentLicense, + capabilities: core.application.capabilities, + }); + + if (!newNavEnabled) { + // TODO: remove block when nav flag no longer needed + this.appUpdater$.next(() => ({ + navLinkStatus: AppNavLinkStatus.hidden, // workaround to prevent main navLink to switch to visible after update. should not be needed + deepLinks: getDeepLinks( + this.experimentalFeatures, + currentLicense.type, + core.application.capabilities + ), + })); + } + } + }); + } else { updateAppLinks(appLinks, { experimentalFeatures: this.experimentalFeatures, - license: currentLicense, capabilities: core.application.capabilities, }); @@ -252,31 +271,14 @@ export class Plugin implements IPlugin ({ - navLinkStatus: AppNavLinkStatus.hidden, // workaround to prevent main navLink to switch to visible after update. should not be needed - deepLinks: getDeepLinks( - this.experimentalFeatures, - undefined, - core.application.capabilities - ), - })); - } - } + }); return {}; } From ac944aa01a8dda41c46d84ccc584b3a6181d6cdf Mon Sep 17 00:00:00 2001 From: semd Date: Wed, 18 May 2022 16:26:34 +0200 Subject: [PATCH 19/25] move links register from start to mount --- .../security_solution/public/plugin.tsx | 123 +++++++++--------- 1 file changed, 65 insertions(+), 58 deletions(-) diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index a82c8c68ffe7e..ddae778b5e494 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -94,6 +94,11 @@ export class Plugin implements IPlugin, plugins: SetupPlugins): PluginSetup { initTelemetry( { @@ -150,6 +155,7 @@ export class Plugin implements IPlugin { - getAppLinks(core, plugins).then((appLinks) => { - if (licensing !== null) { - this.licensingSubscription = licensing.subscribe((currentLicense) => { - if (currentLicense.type !== undefined) { - updateAppLinks(appLinks, { - experimentalFeatures: this.experimentalFeatures, - license: currentLicense, - capabilities: core.application.capabilities, - }); - - if (!newNavEnabled) { - // TODO: remove block when nav flag no longer needed - this.appUpdater$.next(() => ({ - navLinkStatus: AppNavLinkStatus.hidden, // workaround to prevent main navLink to switch to visible after update. should not be needed - deepLinks: getDeepLinks( - this.experimentalFeatures, - currentLicense.type, - core.application.capabilities - ), - })); - } - } - }); - } else { - updateAppLinks(appLinks, { - experimentalFeatures: this.experimentalFeatures, - capabilities: core.application.capabilities, - }); - - if (!newNavEnabled) { - // TODO: remove block when nav flag no longer needed - this.appUpdater$.next(() => ({ - navLinkStatus: AppNavLinkStatus.hidden, // workaround to prevent main navLink to switch to visible after update. should not be needed - deepLinks: getDeepLinks( - this.experimentalFeatures, - undefined, - core.application.capabilities - ), - })); - } - } - }); - }); return {}; } @@ -494,4 +442,63 @@ export class Plugin implements IPlugin { + if (this._linksRegistered) { + return; + } + // Will need reload after settings toggle change, to take fill effect on deepLinks + const newNavEnabled = coreStart.uiSettings.get(ENABLE_GROUPED_NAVIGATION, false); + + const { capabilities } = coreStart.application; + + licenseService.start(startPlugins.licensing.license$); + const licensing = licenseService.getLicenseInformation$(); + + const { getAppLinks } = await this.lazyApplicationLinks(); + const appLinks = await getAppLinks(coreStart, startPlugins); + + // Register deepLinks and pass an appUpdater, any change in appLinks will be reflected to the plugin deepLinks. + if (newNavEnabled) { + registerDeepLinksUpdater(this.appUpdater$); + } + + if (licensing !== null) { + this.licensingSubscription = licensing.subscribe((currentLicense) => { + if (currentLicense.type !== undefined) { + updateAppLinks(appLinks, { + experimentalFeatures: this.experimentalFeatures, + license: currentLicense, + capabilities, + }); + + if (!newNavEnabled) { + // TODO: remove block when nav flag no longer needed + this.appUpdater$.next(() => ({ + navLinkStatus: AppNavLinkStatus.hidden, // workaround to prevent main navLink to switch to visible after update. should not be needed + deepLinks: getDeepLinks(this.experimentalFeatures, currentLicense.type, capabilities), + })); + } + } + }); + } else { + updateAppLinks(appLinks, { + experimentalFeatures: this.experimentalFeatures, + capabilities, + }); + + if (!newNavEnabled) { + // TODO: remove block when nav flag no longer needed + this.appUpdater$.next(() => ({ + navLinkStatus: AppNavLinkStatus.hidden, // workaround to prevent main navLink to switch to visible after update. should not be needed + deepLinks: getDeepLinks(this.experimentalFeatures, undefined, capabilities), + })); + } + } + + this._linksRegistered = true; + } } From c49f7311165ca8213eb1712709db68c3be4551f3 Mon Sep 17 00:00:00 2001 From: semd Date: Wed, 18 May 2022 17:25:42 +0200 Subject: [PATCH 20/25] restore size limits --- packages/kbn-optimizer/limits.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 441227b2e378d..f08fb86c81ab9 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -58,7 +58,7 @@ pageLoadAssetSize: telemetry: 51957 telemetryManagementSection: 38586 transform: 41007 - triggersActionsUi: 105800 #This is temporary. Check https://github.com/elastic/kibana/pull/130710#issuecomment-1119843458 & https://github.com/elastic/kibana/issues/130728 + triggersActionsUi: 105800 upgradeAssistant: 81241 urlForwarding: 32579 usageCollection: 39762 @@ -93,7 +93,7 @@ pageLoadAssetSize: expressionShape: 34008 interactiveSetup: 80000 expressionTagcloud: 27505 - securitySolution: 289364 # This is temporary: https://github.com/elastic/kibana/pull/132210 update-limits when feature flag removed and deprecated code cleaned + securitySolution: 273763 customIntegrations: 28810 expressionMetricVis: 23121 expressionHeatmap: 27505 From ed4cda9ca47e94f2a907f199a740d936c9268f3c Mon Sep 17 00:00:00 2001 From: Pablo Neves Machado Date: Thu, 19 May 2022 13:48:58 +0200 Subject: [PATCH 21/25] Fix use_show_timeline unit tests --- .../utils/timeline/use_show_timeline.test.tsx | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx b/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx index 33a9f3a37a42f..ca9029c6c0939 100644 --- a/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/common/utils/timeline/use_show_timeline.test.tsx @@ -6,7 +6,12 @@ */ import { renderHook, act } from '@testing-library/react-hooks'; +import { coreMock } from '@kbn/core/public/mocks'; +import { allowedExperimentalValues } from '../../../../common/experimental_features'; +import { updateAppLinks } from '../../links'; +import { getAppLinks } from '../../links/app_links'; import { useShowTimeline } from './use_show_timeline'; +import { StartPlugins } from '../../../types'; const mockUseLocation = jest.fn().mockReturnValue({ pathname: '/overview' }); jest.mock('react-router-dom', () => { @@ -24,6 +29,23 @@ jest.mock('../../components/navigation/helpers', () => ({ })); describe('use show timeline', () => { + beforeAll(async () => { + // initialize all App links before running test + const appLinks = await getAppLinks(coreMock.createStart(), {} as StartPlugins); + updateAppLinks(appLinks, { + experimentalFeatures: allowedExperimentalValues, + capabilities: { + navLinks: {}, + management: {}, + catalogue: {}, + actions: { show: true, crud: true }, + siem: { + show: true, + crud: true, + }, + }, + }); + }); describe('useIsGroupedNavigationEnabled false', () => { beforeAll(() => { mockedUseIsGroupedNavigationEnabled.mockReturnValue(false); From b6e260e773d014da1fc395ce4b4268601397605a Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Thu, 19 May 2022 13:41:02 -0700 Subject: [PATCH 22/25] reverted changes where we moved the navigation registration to the component mount - this caused the problem when we do not start from the security app we never see that in the navigation --- .../security_solution/public/plugin.tsx | 124 +++++++++--------- 1 file changed, 59 insertions(+), 65 deletions(-) diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index ddae778b5e494..911a80dd2a684 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -94,11 +94,6 @@ export class Plugin implements IPlugin, plugins: SetupPlugins): PluginSetup { initTelemetry( { @@ -155,7 +150,6 @@ export class Plugin implements IPlugin { + getAppLinks(core, plugins).then((appLinks) => { + if (licensing !== null) { + this.licensingSubscription = licensing.subscribe((currentLicense) => { + if (currentLicense.type !== undefined) { + updateAppLinks(appLinks, { + experimentalFeatures: this.experimentalFeatures, + license: currentLicense, + capabilities: core.application.capabilities, + }); + + if (!newNavEnabled) { + // TODO: remove block when nav flag no longer needed + this.appUpdater$.next(() => ({ + navLinkStatus: AppNavLinkStatus.hidden, // workaround to prevent main navLink to switch to visible after update. should not be needed + deepLinks: getDeepLinks( + this.experimentalFeatures, + currentLicense.type, + core.application.capabilities + ), + })); + } + } + }); + } else { + updateAppLinks(appLinks, { + experimentalFeatures: this.experimentalFeatures, + capabilities: core.application.capabilities, + }); + + if (!newNavEnabled) { + // TODO: remove block when nav flag no longer needed + this.appUpdater$.next(() => ({ + navLinkStatus: AppNavLinkStatus.hidden, // workaround to prevent main navLink to switch to visible after update. should not be needed + deepLinks: getDeepLinks( + this.experimentalFeatures, + undefined, + core.application.capabilities + ), + })); + } + } + }); + }); + return {}; } @@ -442,63 +495,4 @@ export class Plugin implements IPlugin { - if (this._linksRegistered) { - return; - } - // Will need reload after settings toggle change, to take fill effect on deepLinks - const newNavEnabled = coreStart.uiSettings.get(ENABLE_GROUPED_NAVIGATION, false); - - const { capabilities } = coreStart.application; - - licenseService.start(startPlugins.licensing.license$); - const licensing = licenseService.getLicenseInformation$(); - - const { getAppLinks } = await this.lazyApplicationLinks(); - const appLinks = await getAppLinks(coreStart, startPlugins); - - // Register deepLinks and pass an appUpdater, any change in appLinks will be reflected to the plugin deepLinks. - if (newNavEnabled) { - registerDeepLinksUpdater(this.appUpdater$); - } - - if (licensing !== null) { - this.licensingSubscription = licensing.subscribe((currentLicense) => { - if (currentLicense.type !== undefined) { - updateAppLinks(appLinks, { - experimentalFeatures: this.experimentalFeatures, - license: currentLicense, - capabilities, - }); - - if (!newNavEnabled) { - // TODO: remove block when nav flag no longer needed - this.appUpdater$.next(() => ({ - navLinkStatus: AppNavLinkStatus.hidden, // workaround to prevent main navLink to switch to visible after update. should not be needed - deepLinks: getDeepLinks(this.experimentalFeatures, currentLicense.type, capabilities), - })); - } - } - }); - } else { - updateAppLinks(appLinks, { - experimentalFeatures: this.experimentalFeatures, - capabilities, - }); - - if (!newNavEnabled) { - // TODO: remove block when nav flag no longer needed - this.appUpdater$.next(() => ({ - navLinkStatus: AppNavLinkStatus.hidden, // workaround to prevent main navLink to switch to visible after update. should not be needed - deepLinks: getDeepLinks(this.experimentalFeatures, undefined, capabilities), - })); - } - } - - this._linksRegistered = true; - } } From 0f3da0eec82a27099a16276a2588bd2de37c1b44 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 19 May 2022 21:51:04 +0000 Subject: [PATCH 23/25] [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' --- .../security_solution/public/plugin.tsx | 114 +++++++++--------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 911a80dd2a684..3d8e31ed72451 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -223,63 +223,63 @@ export class Plugin implements IPlugin { - getAppLinks(core, plugins).then((appLinks) => { - if (licensing !== null) { - this.licensingSubscription = licensing.subscribe((currentLicense) => { - if (currentLicense.type !== undefined) { - updateAppLinks(appLinks, { - experimentalFeatures: this.experimentalFeatures, - license: currentLicense, - capabilities: core.application.capabilities, - }); - - if (!newNavEnabled) { - // TODO: remove block when nav flag no longer needed - this.appUpdater$.next(() => ({ - navLinkStatus: AppNavLinkStatus.hidden, // workaround to prevent main navLink to switch to visible after update. should not be needed - deepLinks: getDeepLinks( - this.experimentalFeatures, - currentLicense.type, - core.application.capabilities - ), - })); - } - } - }); - } else { - updateAppLinks(appLinks, { - experimentalFeatures: this.experimentalFeatures, - capabilities: core.application.capabilities, - }); - - if (!newNavEnabled) { - // TODO: remove block when nav flag no longer needed - this.appUpdater$.next(() => ({ - navLinkStatus: AppNavLinkStatus.hidden, // workaround to prevent main navLink to switch to visible after update. should not be needed - deepLinks: getDeepLinks( - this.experimentalFeatures, - undefined, - core.application.capabilities - ), - })); - } - } - }); - }); + const licensing = licenseService.getLicenseInformation$(); + + const newNavEnabled = core.uiSettings.get(ENABLE_GROUPED_NAVIGATION, false); + + /** + * Register deepLinks and pass an appUpdater for each subPlugin, to change deepLinks as needed when licensing changes. + */ + + if (newNavEnabled) { + registerDeepLinksUpdater(this.appUpdater$); + } + + // Not using await to prevent blocking start execution + this.lazyApplicationLinks().then(({ getAppLinks }) => { + getAppLinks(core, plugins).then((appLinks) => { + if (licensing !== null) { + this.licensingSubscription = licensing.subscribe((currentLicense) => { + if (currentLicense.type !== undefined) { + updateAppLinks(appLinks, { + experimentalFeatures: this.experimentalFeatures, + license: currentLicense, + capabilities: core.application.capabilities, + }); + + if (!newNavEnabled) { + // TODO: remove block when nav flag no longer needed + this.appUpdater$.next(() => ({ + navLinkStatus: AppNavLinkStatus.hidden, // workaround to prevent main navLink to switch to visible after update. should not be needed + deepLinks: getDeepLinks( + this.experimentalFeatures, + currentLicense.type, + core.application.capabilities + ), + })); + } + } + }); + } else { + updateAppLinks(appLinks, { + experimentalFeatures: this.experimentalFeatures, + capabilities: core.application.capabilities, + }); + + if (!newNavEnabled) { + // TODO: remove block when nav flag no longer needed + this.appUpdater$.next(() => ({ + navLinkStatus: AppNavLinkStatus.hidden, // workaround to prevent main navLink to switch to visible after update. should not be needed + deepLinks: getDeepLinks( + this.experimentalFeatures, + undefined, + core.application.capabilities + ), + })); + } + } + }); + }); return {}; } From 8436ec496707239af57b06198e1b4243d2ada498 Mon Sep 17 00:00:00 2001 From: Pablo Neves Machado Date: Fri, 20 May 2022 10:11:35 +0200 Subject: [PATCH 24/25] Fix HostIsolationExceptions link visibility The Problem: "In the scenario user does not have isolate privileges but there is at least one host isolation exception, this hook will return false at the beginning and true after the data has been loaded." https://github.com/elastic/kibana/pull/132210#discussion_r877177642 Temporary fix: Delete code that removes HostIsolationExceptions link from Applinks and only remove the link from Security navigation. We will investigate a better solution to remove HostIsolationExceptions from AppLinks. --- .../security_side_nav.test.tsx | 75 ++++++++++++++++++- .../security_side_nav/security_side_nav.tsx | 25 ++++--- .../landing_pages/pages/manage.test.tsx | 65 ++++++++++++++++ .../public/landing_pages/pages/manage.tsx | 7 +- 4 files changed, 158 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.test.tsx index da6cfd57ba109..c0ebd0722f725 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.test.tsx @@ -40,8 +40,10 @@ const mockUseRouteSpy = jest.fn(() => [{ pageName: SecurityPageName.alerts }]); jest.mock('../../../utils/route/use_route_spy', () => ({ useRouteSpy: () => mockUseRouteSpy(), })); + +const mockedUseCanSeeHostIsolationExceptionsMenu = jest.fn(); jest.mock('../../../../management/pages/host_isolation_exceptions/view/hooks', () => ({ - useCanSeeHostIsolationExceptionsMenu: () => true, + useCanSeeHostIsolationExceptionsMenu: () => mockedUseCanSeeHostIsolationExceptionsMenu(), })); jest.mock('../../../links', () => ({ getAncestorLinksInfo: (id: string) => [{ id }], @@ -162,6 +164,77 @@ describe('SecuritySideNav', () => { ); }); + it('should render hostIsolationExceptionsLink when useCanSeeHostIsolationExceptionsMenu is true', () => { + mockedUseCanSeeHostIsolationExceptionsMenu.mockReturnValue(true); + const hostIsolationExceptionsLink = { + id: SecurityPageName.hostIsolationExceptions, + title: 'test hostIsolationExceptions', + description: 'test description hostIsolationExceptions', + }; + + mockUseAppNavLinks.mockReturnValueOnce([ + { + ...manageNavLink, + links: [hostIsolationExceptionsLink], + }, + ]); + renderNav(); + expect(mockSolutionGroupedNav).toHaveBeenCalledWith( + expect.objectContaining({ + items: [], + selectedId: SecurityPageName.alerts, + footerItems: [ + { + id: SecurityPageName.administration, + label: 'manage', + href: '/administration', + categories: manageNavLink.categories, + items: [ + { + id: hostIsolationExceptionsLink.id, + label: hostIsolationExceptionsLink.title, + description: hostIsolationExceptionsLink.description, + href: '/host_isolation_exceptions', + }, + ], + }, + ], + }) + ); + }); + + it('should not render hostIsolationExceptionsLink when useCanSeeHostIsolationExceptionsMenu is false', () => { + mockedUseCanSeeHostIsolationExceptionsMenu.mockReturnValue(false); + const hostIsolationExceptionsLink = { + id: SecurityPageName.hostIsolationExceptions, + title: 'test hostIsolationExceptions', + description: 'test description hostIsolationExceptions', + }; + + mockUseAppNavLinks.mockReturnValueOnce([ + { + ...manageNavLink, + links: [hostIsolationExceptionsLink], + }, + ]); + renderNav(); + expect(mockSolutionGroupedNav).toHaveBeenCalledWith( + expect.objectContaining({ + items: [], + selectedId: SecurityPageName.alerts, + footerItems: [ + { + id: SecurityPageName.administration, + label: 'manage', + href: '/administration', + categories: manageNavLink.categories, + items: [], + }, + ], + }) + ); + }); + it('should render custom item', () => { mockUseAppNavLinks.mockReturnValueOnce([ { id: SecurityPageName.landing, title: 'get started' }, diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.tsx index 139c9fa4d1acb..b9173270e381e 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.tsx @@ -5,10 +5,10 @@ * 2.0. */ -import React, { useMemo, useCallback, useEffect } from 'react'; +import React, { useMemo, useCallback } from 'react'; import { EuiHorizontalRule, EuiListGroupItem, EuiLoadingSpinner } from '@elastic/eui'; import { SecurityPageName } from '../../../../app/types'; -import { excludeAppLink, getAncestorLinksInfo } from '../../../links'; +import { getAncestorLinksInfo } from '../../../links'; import { useRouteSpy } from '../../../utils/route/use_route_spy'; import { SecuritySolutionLinkAnchor, useGetSecuritySolutionLinkProps } from '../../links'; import { useAppNavLinks } from '../nav_links'; @@ -52,6 +52,7 @@ const GetStartedCustomLink = React.memo(GetStartedCustomLinkComponent); * Returns a function to format generic `NavLinkItem` array to the `SideNavItem` type */ const useFormatSideNavItem = (): FormatSideNavItems => { + const hideHostIsolationExceptions = !useCanSeeHostIsolationExceptionsMenu(); const getSecuritySolutionLinkProps = useGetSecuritySolutionLinkProps(); // adds href and onClick props const formatSideNavItem: FormatSideNavItems = useCallback( @@ -66,7 +67,14 @@ const useFormatSideNavItem = (): FormatSideNavItems => { ...(navItem.links && navItem.links.length > 0 ? { items: navItem.links - .filter((link) => !link.disabled) + .filter( + (link) => + !link.disabled && + !( + link.id === SecurityPageName.hostIsolationExceptions && + hideHostIsolationExceptions + ) + ) .map((panelNavItem) => ({ id: panelNavItem.id, label: panelNavItem.title, @@ -89,7 +97,7 @@ const useFormatSideNavItem = (): FormatSideNavItems => { } return formatDefaultItem(navLinkItem); }, - [getSecuritySolutionLinkProps] + [getSecuritySolutionLinkProps, hideHostIsolationExceptions] ); return formatSideNavItem; @@ -109,6 +117,7 @@ const useSideNavItems = () => { if (appNavLink.disabled) { return; } + if (isFooterNavItem(appNavLink.id)) { footerNavItems.push(formatSideNavItem(appNavLink)); } else { @@ -139,14 +148,6 @@ export const SecuritySideNav: React.FC = () => { const [items, footerItems] = useSideNavItems(); const selectedId = useSelectedId(); - // TODO: move this exclusion logic to getManagementLinkItems - const canSeeHostIsolationExceptions = useCanSeeHostIsolationExceptionsMenu(); - useEffect(() => { - if (!canSeeHostIsolationExceptions) { - excludeAppLink(SecurityPageName.hostIsolationExceptions); - } - }, [canSeeHostIsolationExceptions]); - if (items.length === 0 && footerItems.length === 0) { return ; } diff --git a/x-pack/plugins/security_solution/public/landing_pages/pages/manage.test.tsx b/x-pack/plugins/security_solution/public/landing_pages/pages/manage.test.tsx index 06389105cec2b..a09db6ebf5eaa 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/pages/manage.test.tsx +++ b/x-pack/plugins/security_solution/public/landing_pages/pages/manage.test.tsx @@ -46,6 +46,11 @@ const defaultAppManageLink: NavLinkItem = { ], }; +const mockedUseCanSeeHostIsolationExceptionsMenu = jest.fn(); +jest.mock('../../management/pages/host_isolation_exceptions/view/hooks', () => ({ + useCanSeeHostIsolationExceptionsMenu: () => mockedUseCanSeeHostIsolationExceptionsMenu(), +})); + const mockAppManageLink = jest.fn(() => defaultAppManageLink); jest.mock('../../common/components/navigation/nav_links', () => ({ useAppRootNavLink: () => mockAppManageLink(), @@ -129,4 +134,64 @@ describe('ManagementCategories', () => { expect(queryByText(CATEGORY_1_LABEL)).not.toBeInTheDocument(); expect(queryByText(CATEGORY_2_LABEL)).not.toBeInTheDocument(); }); + + it('should not render hostIsolationExceptionsLink when useCanSeeHostIsolationExceptionsMenu is false', () => { + mockedUseCanSeeHostIsolationExceptionsMenu.mockReturnValueOnce(false); + mockAppManageLink.mockReturnValueOnce({ + ...defaultAppManageLink, + categories: [ + { + label: CATEGORY_1_LABEL, + linkIds: [SecurityPageName.hostIsolationExceptions], + }, + ], + links: [ + { + id: SecurityPageName.hostIsolationExceptions, + title: 'test hostIsolationExceptions title', + description: 'test hostIsolationExceptions description', + icon: 'testIcon1', + }, + ], + }); + const { queryByText } = render( + + + + ); + + expect(queryByText(CATEGORY_1_LABEL)).not.toBeInTheDocument(); + }); + + it('should render hostIsolationExceptionsLink when useCanSeeHostIsolationExceptionsMenu is true', () => { + const HOST_ISOLATION_EXCEPTIONS_ITEM_LABEL = 'test hostIsolationExceptions title'; + mockedUseCanSeeHostIsolationExceptionsMenu.mockReturnValueOnce(true); + mockAppManageLink.mockReturnValueOnce({ + ...defaultAppManageLink, + categories: [ + { + label: CATEGORY_1_LABEL, + linkIds: [SecurityPageName.hostIsolationExceptions], + }, + ], + links: [ + { + id: SecurityPageName.hostIsolationExceptions, + title: HOST_ISOLATION_EXCEPTIONS_ITEM_LABEL, + description: 'test hostIsolationExceptions description', + icon: 'testIcon1', + }, + ], + }); + const { queryAllByTestId } = render( + + + + ); + + const renderedItems = queryAllByTestId('LandingItem'); + + expect(renderedItems).toHaveLength(1); + expect(renderedItems[0]).toHaveTextContent(HOST_ISOLATION_EXCEPTIONS_ITEM_LABEL); + }); }); diff --git a/x-pack/plugins/security_solution/public/landing_pages/pages/manage.tsx b/x-pack/plugins/security_solution/public/landing_pages/pages/manage.tsx index 06bd0c6258b57..d484e5fe90a52 100644 --- a/x-pack/plugins/security_solution/public/landing_pages/pages/manage.tsx +++ b/x-pack/plugins/security_solution/public/landing_pages/pages/manage.tsx @@ -14,6 +14,7 @@ import { useAppRootNavLink } from '../../common/components/navigation/nav_links' import { NavLinkItem } from '../../common/components/navigation/types'; import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; import { SpyRoute } from '../../common/utils/route/spy_routes'; +import { useCanSeeHostIsolationExceptionsMenu } from '../../management/pages/host_isolation_exceptions/view/hooks'; import { LandingLinksIcons } from '../components/landing_links_icons'; import { MANAGE_PAGE_TITLE } from './translations'; @@ -32,13 +33,17 @@ const StyledEuiHorizontalRule = styled(EuiHorizontalRule)` type ManagementCategories = Array<{ label: string; links: NavLinkItem[] }>; const useManagementCategories = (): ManagementCategories => { + const hideHostIsolationExceptions = !useCanSeeHostIsolationExceptionsMenu(); const { links = [], categories = [] } = useAppRootNavLink(SecurityPageName.administration) ?? {}; const manageLinksById = Object.fromEntries(links.map((link) => [link.id, link])); return categories.reduce((acc, { label, linkIds }) => { const linksItem = linkIds.reduce((linksAcc, linkId) => { - if (manageLinksById[linkId]) { + if ( + manageLinksById[linkId] && + !(linkId === SecurityPageName.hostIsolationExceptions && hideHostIsolationExceptions) + ) { linksAcc.push(manageLinksById[linkId]); } return linksAcc; From c25535469e82b281b05d4708612a85cd0d9ec6de Mon Sep 17 00:00:00 2001 From: Pablo Neves Machado Date: Fri, 20 May 2022 13:33:22 +0200 Subject: [PATCH 25/25] Fix manageOldSiemRoutes been called before deepLinks initialization --- x-pack/plugins/security_solution/public/plugin.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 3d8e31ed72451..1716e08febd40 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -49,7 +49,7 @@ import { } from '../common/constants'; import { getDeepLinks, registerDeepLinksUpdater } from './app/deep_links'; -import { updateAppLinks } from './common/links'; +import { AppLinkItems, subscribeAppLinks, updateAppLinks } from './common/links'; import { getSubPluginRoutesByCapabilities, manageOldSiemRoutes } from './helpers'; import { SecurityAppStore } from './common/store/store'; import { licenseService } from './common/hooks/use_license'; @@ -172,7 +172,15 @@ export class Plugin implements IPlugin { const [coreStart] = await core.getStartServices(); - manageOldSiemRoutes(coreStart); + + const subscription = subscribeAppLinks((links: AppLinkItems) => { + // It has to be called once after deep links are initialized + if (links.length > 0) { + manageOldSiemRoutes(coreStart); + subscription.unsubscribe(); + } + }); + return () => true; }, });