[Security Solutions] Side Navigation phase 2 (#103275)

* [SecuritySolutions] [Navigation] Prepare new routing and migrate overview (#101733)

* prepare new routing and migrate overview

* test fix and todo comments identified

* telemetry using app views

* navigation groups implemented

* cleaning

* export subplugin routes as route props array

* [Security Solution][Navigation] Migrate Security Solutions 'explore' tab group to deep link navigation (#102306)

* Update navigateToApp and getUrlForApp to provide the deepLinkId
* Update Hosts and Network routes to start from /hosts and /network
* Add Hosts and Network to side nav menu under "Explore" menu group
* Delete Hosts and Network old menu code
* Fix broken tests

* [SecuritySolution] Add detections subplugin to deeplink (#101791)

* prepare new routing and migrate overview

* init nav deeplink

* split detections into rules and alerts

* init exception link

* init detections

* link to rules creation page

* link to rules creation page

* rename detections to alerts

* fix unit tests

* fix rules creation page

* remove console

* fix lint error

* fix unit tests

* fix unit tests

* isolating rules and exceptions page

* replace history push with navigateToApp

* fix unit test

* temporary fix for createCoreStartMock

* update cypress

* skip failing cypress

* skip failing cypress

Co-authored-by: semd <sergi.massaneda@elastic.co>

* Migrate "Investigate" tab group to new side navigation (#102705)

* Migrate "Investigate" tab group to new side navigation

It includes:
* Timelines
* Cases

* Quick fix useFormatUrl and HeaderPage navigation

* [Security Solutions] Management navigation (#102685)

* prepare new routing and migrate overview

* test fix and todo comments identified

* telemetry using app views

* navigation groups implemented

* cleaning

* export subplugin routes as route props array

* breadcrumbs changes and sidenav generation improvements

* jest tests for breadcrumbs and navigation changes

* retrocompatibility for sections that are not yet migrated to deepLinks

* management deepLinks and plugin refactoring

* home navigation changes

* management navigation migrated to deeplinks

* jest tests fixed

* header page back link improved and tests fixed

* type errors fixes

* improve home navigation encapsulation

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

* Fix type checking

* export header page

* fix padding

* add redirect routes

* unskip detection cypress

* fix i18n

* fix create your own rules btn

* fix cancel button on rules creation page

* test fixes

* fix breadcrumbs for rules pages

* unit test fixes

* additional fixes

* [Security Solutions] Navigation usage tracker and general changes (#103271)

* [Security Solutions] use of currentAppId$ migrated. and some small fixes

* unused constants removed

* remove unused constant

* test fix and types

* fix cypress

* fix cypress tests

* Fix case navTab permission and tests

* Revert 'timeline.isOpen' breadcrumb code that was deleted during merge

* Fix useInsertTimeline test by removing '/'

* change global navigation visible deeplinks

* fix /admininstration top level redirect to

* fix global search icon, nav order and overview hosts link

* update start a new case link

* fix rules link in exception list table

* unskip cypress tests

* update rules link

* fix full screen timeline

* fixing broken links and administration telemetry split

* remove unused comments

* remove timeline z-index and cleanup global header component

* some minor fixes

* add unit tests for detections breadcrumbs

* remove case to  global/search nav when cases is none

* rename test scenario

* fix side_panel flyout

* fix cases use cases between search/gobal nav

* timeline snapshot regenerated and cypres test fixed

* rollback management tracking split as it causes unexpected errors on the telemetry component

Co-authored-by: Pablo Machado <pablo.nevesmachado@elastic.co>
Co-authored-by: Angela Chuang <6295984+angorayc@users.noreply.github.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Michael Olorunnisola <michael.olorunnisola@elastic.co>
Co-authored-by: Angela Chuang <yi-chun.chuang@elastic.co>
Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com>
This commit is contained in:
Sergi Massaneda 2021-06-29 15:00:05 +02:00 committed by GitHub
parent 3b5bd02347
commit 85709925cc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
215 changed files with 3234 additions and 3566 deletions

View file

@ -157,14 +157,7 @@ export const applicationUsageSchema = {
security_login: commonSchema,
security_logout: commonSchema,
security_overwritten_session: commonSchema,
securitySolution: commonSchema, // It's a forward app so we'll likely never report it
'securitySolution:overview': commonSchema,
'securitySolution:detections': commonSchema,
'securitySolution:hosts': commonSchema,
'securitySolution:network': commonSchema,
'securitySolution:timelines': commonSchema,
'securitySolution:case': commonSchema,
'securitySolution:administration': commonSchema,
securitySolution: commonSchema,
siem: commonSchema,
space_selector: commonSchema,
uptime: commonSchema,

View file

@ -5280,923 +5280,6 @@
}
}
},
"securitySolution:overview": {
"properties": {
"appId": {
"type": "keyword",
"_meta": {
"description": "The application being tracked"
}
},
"viewId": {
"type": "keyword",
"_meta": {
"description": "Always `main`"
}
},
"clicks_total": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application since we started counting them"
}
},
"clicks_7_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application over the last 7 days"
}
},
"clicks_30_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application over the last 30 days"
}
},
"clicks_90_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application over the last 90 days"
}
},
"minutes_on_screen_total": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen since we started counting them."
}
},
"minutes_on_screen_7_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen over the last 7 days"
}
},
"minutes_on_screen_30_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen over the last 30 days"
}
},
"minutes_on_screen_90_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen over the last 90 days"
}
},
"views": {
"type": "array",
"items": {
"properties": {
"appId": {
"type": "keyword",
"_meta": {
"description": "The application being tracked"
}
},
"viewId": {
"type": "keyword",
"_meta": {
"description": "The application view being tracked"
}
},
"clicks_total": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application sub view since we started counting them"
}
},
"clicks_7_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the active application sub view over the last 7 days"
}
},
"clicks_30_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the active application sub view over the last 30 days"
}
},
"clicks_90_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the active application sub view over the last 90 days"
}
},
"minutes_on_screen_total": {
"type": "float",
"_meta": {
"description": "Minutes the application sub view is active and on-screen since we started counting them."
}
},
"minutes_on_screen_7_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen active application sub view over the last 7 days"
}
},
"minutes_on_screen_30_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen active application sub view over the last 30 days"
}
},
"minutes_on_screen_90_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen active application sub view over the last 90 days"
}
}
}
}
}
}
},
"securitySolution:detections": {
"properties": {
"appId": {
"type": "keyword",
"_meta": {
"description": "The application being tracked"
}
},
"viewId": {
"type": "keyword",
"_meta": {
"description": "Always `main`"
}
},
"clicks_total": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application since we started counting them"
}
},
"clicks_7_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application over the last 7 days"
}
},
"clicks_30_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application over the last 30 days"
}
},
"clicks_90_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application over the last 90 days"
}
},
"minutes_on_screen_total": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen since we started counting them."
}
},
"minutes_on_screen_7_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen over the last 7 days"
}
},
"minutes_on_screen_30_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen over the last 30 days"
}
},
"minutes_on_screen_90_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen over the last 90 days"
}
},
"views": {
"type": "array",
"items": {
"properties": {
"appId": {
"type": "keyword",
"_meta": {
"description": "The application being tracked"
}
},
"viewId": {
"type": "keyword",
"_meta": {
"description": "The application view being tracked"
}
},
"clicks_total": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application sub view since we started counting them"
}
},
"clicks_7_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the active application sub view over the last 7 days"
}
},
"clicks_30_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the active application sub view over the last 30 days"
}
},
"clicks_90_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the active application sub view over the last 90 days"
}
},
"minutes_on_screen_total": {
"type": "float",
"_meta": {
"description": "Minutes the application sub view is active and on-screen since we started counting them."
}
},
"minutes_on_screen_7_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen active application sub view over the last 7 days"
}
},
"minutes_on_screen_30_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen active application sub view over the last 30 days"
}
},
"minutes_on_screen_90_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen active application sub view over the last 90 days"
}
}
}
}
}
}
},
"securitySolution:hosts": {
"properties": {
"appId": {
"type": "keyword",
"_meta": {
"description": "The application being tracked"
}
},
"viewId": {
"type": "keyword",
"_meta": {
"description": "Always `main`"
}
},
"clicks_total": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application since we started counting them"
}
},
"clicks_7_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application over the last 7 days"
}
},
"clicks_30_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application over the last 30 days"
}
},
"clicks_90_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application over the last 90 days"
}
},
"minutes_on_screen_total": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen since we started counting them."
}
},
"minutes_on_screen_7_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen over the last 7 days"
}
},
"minutes_on_screen_30_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen over the last 30 days"
}
},
"minutes_on_screen_90_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen over the last 90 days"
}
},
"views": {
"type": "array",
"items": {
"properties": {
"appId": {
"type": "keyword",
"_meta": {
"description": "The application being tracked"
}
},
"viewId": {
"type": "keyword",
"_meta": {
"description": "The application view being tracked"
}
},
"clicks_total": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application sub view since we started counting them"
}
},
"clicks_7_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the active application sub view over the last 7 days"
}
},
"clicks_30_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the active application sub view over the last 30 days"
}
},
"clicks_90_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the active application sub view over the last 90 days"
}
},
"minutes_on_screen_total": {
"type": "float",
"_meta": {
"description": "Minutes the application sub view is active and on-screen since we started counting them."
}
},
"minutes_on_screen_7_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen active application sub view over the last 7 days"
}
},
"minutes_on_screen_30_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen active application sub view over the last 30 days"
}
},
"minutes_on_screen_90_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen active application sub view over the last 90 days"
}
}
}
}
}
}
},
"securitySolution:network": {
"properties": {
"appId": {
"type": "keyword",
"_meta": {
"description": "The application being tracked"
}
},
"viewId": {
"type": "keyword",
"_meta": {
"description": "Always `main`"
}
},
"clicks_total": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application since we started counting them"
}
},
"clicks_7_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application over the last 7 days"
}
},
"clicks_30_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application over the last 30 days"
}
},
"clicks_90_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application over the last 90 days"
}
},
"minutes_on_screen_total": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen since we started counting them."
}
},
"minutes_on_screen_7_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen over the last 7 days"
}
},
"minutes_on_screen_30_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen over the last 30 days"
}
},
"minutes_on_screen_90_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen over the last 90 days"
}
},
"views": {
"type": "array",
"items": {
"properties": {
"appId": {
"type": "keyword",
"_meta": {
"description": "The application being tracked"
}
},
"viewId": {
"type": "keyword",
"_meta": {
"description": "The application view being tracked"
}
},
"clicks_total": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application sub view since we started counting them"
}
},
"clicks_7_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the active application sub view over the last 7 days"
}
},
"clicks_30_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the active application sub view over the last 30 days"
}
},
"clicks_90_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the active application sub view over the last 90 days"
}
},
"minutes_on_screen_total": {
"type": "float",
"_meta": {
"description": "Minutes the application sub view is active and on-screen since we started counting them."
}
},
"minutes_on_screen_7_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen active application sub view over the last 7 days"
}
},
"minutes_on_screen_30_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen active application sub view over the last 30 days"
}
},
"minutes_on_screen_90_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen active application sub view over the last 90 days"
}
}
}
}
}
}
},
"securitySolution:timelines": {
"properties": {
"appId": {
"type": "keyword",
"_meta": {
"description": "The application being tracked"
}
},
"viewId": {
"type": "keyword",
"_meta": {
"description": "Always `main`"
}
},
"clicks_total": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application since we started counting them"
}
},
"clicks_7_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application over the last 7 days"
}
},
"clicks_30_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application over the last 30 days"
}
},
"clicks_90_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application over the last 90 days"
}
},
"minutes_on_screen_total": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen since we started counting them."
}
},
"minutes_on_screen_7_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen over the last 7 days"
}
},
"minutes_on_screen_30_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen over the last 30 days"
}
},
"minutes_on_screen_90_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen over the last 90 days"
}
},
"views": {
"type": "array",
"items": {
"properties": {
"appId": {
"type": "keyword",
"_meta": {
"description": "The application being tracked"
}
},
"viewId": {
"type": "keyword",
"_meta": {
"description": "The application view being tracked"
}
},
"clicks_total": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application sub view since we started counting them"
}
},
"clicks_7_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the active application sub view over the last 7 days"
}
},
"clicks_30_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the active application sub view over the last 30 days"
}
},
"clicks_90_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the active application sub view over the last 90 days"
}
},
"minutes_on_screen_total": {
"type": "float",
"_meta": {
"description": "Minutes the application sub view is active and on-screen since we started counting them."
}
},
"minutes_on_screen_7_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen active application sub view over the last 7 days"
}
},
"minutes_on_screen_30_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen active application sub view over the last 30 days"
}
},
"minutes_on_screen_90_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen active application sub view over the last 90 days"
}
}
}
}
}
}
},
"securitySolution:case": {
"properties": {
"appId": {
"type": "keyword",
"_meta": {
"description": "The application being tracked"
}
},
"viewId": {
"type": "keyword",
"_meta": {
"description": "Always `main`"
}
},
"clicks_total": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application since we started counting them"
}
},
"clicks_7_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application over the last 7 days"
}
},
"clicks_30_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application over the last 30 days"
}
},
"clicks_90_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application over the last 90 days"
}
},
"minutes_on_screen_total": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen since we started counting them."
}
},
"minutes_on_screen_7_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen over the last 7 days"
}
},
"minutes_on_screen_30_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen over the last 30 days"
}
},
"minutes_on_screen_90_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen over the last 90 days"
}
},
"views": {
"type": "array",
"items": {
"properties": {
"appId": {
"type": "keyword",
"_meta": {
"description": "The application being tracked"
}
},
"viewId": {
"type": "keyword",
"_meta": {
"description": "The application view being tracked"
}
},
"clicks_total": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application sub view since we started counting them"
}
},
"clicks_7_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the active application sub view over the last 7 days"
}
},
"clicks_30_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the active application sub view over the last 30 days"
}
},
"clicks_90_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the active application sub view over the last 90 days"
}
},
"minutes_on_screen_total": {
"type": "float",
"_meta": {
"description": "Minutes the application sub view is active and on-screen since we started counting them."
}
},
"minutes_on_screen_7_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen active application sub view over the last 7 days"
}
},
"minutes_on_screen_30_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen active application sub view over the last 30 days"
}
},
"minutes_on_screen_90_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen active application sub view over the last 90 days"
}
}
}
}
}
}
},
"securitySolution:administration": {
"properties": {
"appId": {
"type": "keyword",
"_meta": {
"description": "The application being tracked"
}
},
"viewId": {
"type": "keyword",
"_meta": {
"description": "Always `main`"
}
},
"clicks_total": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application since we started counting them"
}
},
"clicks_7_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application over the last 7 days"
}
},
"clicks_30_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application over the last 30 days"
}
},
"clicks_90_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application over the last 90 days"
}
},
"minutes_on_screen_total": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen since we started counting them."
}
},
"minutes_on_screen_7_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen over the last 7 days"
}
},
"minutes_on_screen_30_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen over the last 30 days"
}
},
"minutes_on_screen_90_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen over the last 90 days"
}
},
"views": {
"type": "array",
"items": {
"properties": {
"appId": {
"type": "keyword",
"_meta": {
"description": "The application being tracked"
}
},
"viewId": {
"type": "keyword",
"_meta": {
"description": "The application view being tracked"
}
},
"clicks_total": {
"type": "long",
"_meta": {
"description": "General number of clicks in the application sub view since we started counting them"
}
},
"clicks_7_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the active application sub view over the last 7 days"
}
},
"clicks_30_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the active application sub view over the last 30 days"
}
},
"clicks_90_days": {
"type": "long",
"_meta": {
"description": "General number of clicks in the active application sub view over the last 90 days"
}
},
"minutes_on_screen_total": {
"type": "float",
"_meta": {
"description": "Minutes the application sub view is active and on-screen since we started counting them."
}
},
"minutes_on_screen_7_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen active application sub view over the last 7 days"
}
},
"minutes_on_screen_30_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen active application sub view over the last 30 days"
}
},
"minutes_on_screen_90_days": {
"type": "float",
"_meta": {
"description": "Minutes the application is active and on-screen active application sub view over the last 90 days"
}
}
}
}
}
}
},
"siem": {
"properties": {
"appId": {

View file

@ -5,10 +5,11 @@
* 2.0.
*/
import React from 'react';
import React, { useCallback } from 'react';
import { EuiLink } from '@elastic/eui';
import * as i18n from '../translations';
import { useKibana } from '../../../common/lib/kibana';
import { LinkAnchor } from '../../links';
const NoCasesComponent = ({
createCaseHref,
@ -17,13 +18,22 @@ const NoCasesComponent = ({
createCaseHref: string;
hasWritePermissions: boolean;
}) => {
const { navigateToUrl } = useKibana().services.application;
const goToCaseCreation = useCallback(
(e) => {
e.preventDefault();
navigateToUrl(createCaseHref);
},
[createCaseHref, navigateToUrl]
);
return hasWritePermissions ? (
<>
<span>{i18n.NO_CASES}</span>
<EuiLink
<LinkAnchor
data-test-subj="no-cases-create-case"
onClick={goToCaseCreation}
href={createCaseHref}
>{` ${i18n.START_A_NEW_CASE}`}</EuiLink>
>{` ${i18n.START_A_NEW_CASE}`}</LinkAnchor>
{'!'}
</>
) : (

View file

@ -62,29 +62,57 @@ export const DEFAULT_INDICATOR_SOURCE_PATH = 'threatintel.indicator';
export const INDICATOR_DESTINATION_PATH = 'threat.indicator';
export enum SecurityPageName {
detections = 'detections',
overview = 'overview',
detections = 'detections',
alerts = 'alerts',
rules = 'rules',
exceptions = 'exceptions',
hosts = 'hosts',
network = 'network',
timelines = 'timelines',
case = 'case',
administration = 'administration',
endpoints = 'endpoints',
policies = 'policies',
trustedApps = 'trusted_apps',
eventFilters = 'event_filters',
}
/**
* The ID of the cases plugin
*/
export const CASES_APP_ID = `${APP_ID}:${SecurityPageName.case}`;
export enum SecurityPageGroupName {
detect = 'detect',
explore = 'explore',
investigate = 'investigate',
manage = 'manage',
}
export const APP_OVERVIEW_PATH = `${APP_PATH}/overview`;
export const APP_DETECTIONS_PATH = `${APP_PATH}/detections`;
export const APP_HOSTS_PATH = `${APP_PATH}/hosts`;
export const APP_NETWORK_PATH = `${APP_PATH}/network`;
export const APP_TIMELINES_PATH = `${APP_PATH}/timelines`;
export const APP_CASES_PATH = `${APP_PATH}/cases`;
export const APP_MANAGEMENT_PATH = `${APP_PATH}/administration`;
export const TIMELINES_PATH = '/timelines';
export const CASES_PATH = '/cases';
export const OVERVIEW_PATH = '/overview';
export const DETECTIONS_PATH = '/detections';
export const ALERTS_PATH = '/alerts';
export const RULES_PATH = '/rules';
export const EXCEPTIONS_PATH = '/exceptions';
export const HOSTS_PATH = '/hosts';
export const NETWORK_PATH = '/network';
export const MANAGEMENT_PATH = '/administration';
export const ENDPOINTS_PATH = `${MANAGEMENT_PATH}/endpoints`;
export const TRUSTED_APPS_PATH = `${MANAGEMENT_PATH}/trusted_apps`;
export const EVENT_FILTERS_PATH = `${MANAGEMENT_PATH}/event_filters`;
export const DETECTIONS_SUB_PLUGIN_ID = `${APP_ID}:${SecurityPageName.detections}`;
export const APP_OVERVIEW_PATH = `${APP_PATH}${OVERVIEW_PATH}`;
export const APP_MANAGEMENT_PATH = `${APP_PATH}${MANAGEMENT_PATH}`;
export const APP_ALERTS_PATH = `${APP_PATH}${ALERTS_PATH}`;
export const APP_RULES_PATH = `${APP_PATH}${RULES_PATH}`;
export const APP_EXCEPTIONS_PATH = `${APP_PATH}${EXCEPTIONS_PATH}`;
export const APP_HOSTS_PATH = `${APP_PATH}${HOSTS_PATH}`;
export const APP_NETWORK_PATH = `${APP_PATH}${NETWORK_PATH}`;
export const APP_TIMELINES_PATH = `${APP_PATH}${TIMELINES_PATH}`;
export const APP_CASES_PATH = `${APP_PATH}${CASES_PATH}`;
export const APP_ENDPOINTS_PATH = `${APP_PATH}${ENDPOINTS_PATH}`;
export const APP_TRUSTED_APPS_PATH = `${APP_PATH}${TRUSTED_APPS_PATH}`;
export const APP_EVENT_FILTERS_PATH = `${APP_PATH}${EVENT_FILTERS_PATH}`;
/** The comma-delimited list of Elasticsearch indices from which the SIEM app collects events */
export const DEFAULT_INDEX_PATTERN = [

View file

@ -21,11 +21,13 @@ import { createCase } from '../../tasks/api_calls/cases';
describe('attach timeline to case', () => {
context('without cases created', () => {
beforeEach(() => {
beforeEach((done) => {
cleanKibana();
createTimeline(timeline).then((response) =>
cy.wrap(response.body.data.persistTimeline.timeline).as('myTimeline')
);
createTimeline(timeline).then((response) => {
cy.wrap(response.body.data.persistTimeline.timeline).as('myTimeline');
done();
});
});
it('attach timeline to a new case', function () {

View file

@ -20,17 +20,17 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import { unmappedRule } from '../../objects/rule';
import { DETECTIONS_URL } from '../../urls/navigation';
import { ALERTS_URL } from '../../urls/navigation';
describe('Alert details with unmapped fields', () => {
beforeEach(() => {
cleanKibana();
esArchiverLoad('unmapped_fields');
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
loginAndWaitForPageWithoutDateRange(ALERTS_URL);
waitForAlertsPanelToBeLoaded();
waitForAlertsIndexToBeCreated();
createCustomRuleActivated(unmappedRule);
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
loginAndWaitForPageWithoutDateRange(ALERTS_URL);
waitForAlertsPanelToBeLoaded();
expandFirstAlert();
});

View file

@ -6,7 +6,7 @@
*/
import { ROLES } from '../../../common/test';
import { DETECTIONS_RULE_MANAGEMENT_URL, DETECTIONS_URL } from '../../urls/navigation';
import { DETECTIONS_RULE_MANAGEMENT_URL, ALERTS_URL } from '../../urls/navigation';
import { newRule } from '../../objects/rule';
import { PAGE_TITLE } from '../../screens/common/page';
@ -37,7 +37,7 @@ describe('Detections > Need Admin Callouts indicating an admin is needed to migr
// First, we have to open the app on behalf of a privileged user in order to initialize it.
// Otherwise the app will be disabled and show a "welcome"-like page.
cleanKibana();
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL, ROLES.platform_engineer);
loginAndWaitForPageWithoutDateRange(ALERTS_URL, ROLES.platform_engineer);
waitForAlertsIndexToBeCreated();
// After that we can login as a soc manager.
@ -57,7 +57,7 @@ describe('Detections > Need Admin Callouts indicating an admin is needed to migr
});
context('On Detections home page', () => {
beforeEach(() => {
loadPageAsPlatformEngineerUser(DETECTIONS_URL);
loadPageAsPlatformEngineerUser(ALERTS_URL);
});
it('We show the need admin primary callout', () => {
@ -107,7 +107,7 @@ describe('Detections > Need Admin Callouts indicating an admin is needed to migr
});
context('On Detections home page', () => {
beforeEach(() => {
loadPageAsPlatformEngineerUser(DETECTIONS_URL);
loadPageAsPlatformEngineerUser(ALERTS_URL);
});
it('We show the need admin primary callout', () => {
@ -157,7 +157,7 @@ describe('Detections > Need Admin Callouts indicating an admin is needed to migr
});
context('On Detections home page', () => {
beforeEach(() => {
loadPageAsPlatformEngineerUser(DETECTIONS_URL);
loadPageAsPlatformEngineerUser(ALERTS_URL);
});
it('We show the need admin primary callout', () => {

View file

@ -15,11 +15,11 @@ import { waitForAlertsToPopulate } from '../../tasks/create_new_rule';
import { login, loginAndWaitForPage, waitForPageWithoutDateRange } from '../../tasks/login';
import { refreshPage } from '../../tasks/security_header';
import { DETECTIONS_URL } from '../../urls/navigation';
import { ALERTS_URL } from '../../urls/navigation';
import { ATTACH_ALERT_TO_CASE_BUTTON } from '../../screens/alerts_detection_rules';
const loadDetectionsPage = (role: ROLES) => {
waitForPageWithoutDateRange(DETECTIONS_URL, role);
waitForPageWithoutDateRange(ALERTS_URL, role);
waitForAlertsToPopulate();
};
@ -27,7 +27,7 @@ describe('Alerts timeline', () => {
before(() => {
// First we login as a privileged user to create alerts.
cleanKibana();
loginAndWaitForPage(DETECTIONS_URL, ROLES.platform_engineer);
loginAndWaitForPage(ALERTS_URL, ROLES.platform_engineer);
waitForAlertsPanelToBeLoaded();
waitForAlertsIndexToBeCreated();
createCustomRuleActivated(newRule);

View file

@ -31,12 +31,12 @@ import { waitForAlertsToPopulate } from '../../tasks/create_new_rule';
import { loginAndWaitForPage } from '../../tasks/login';
import { refreshPage } from '../../tasks/security_header';
import { DETECTIONS_URL } from '../../urls/navigation';
import { ALERTS_URL } from '../../urls/navigation';
describe('Closing alerts', () => {
beforeEach(() => {
cleanKibana();
loginAndWaitForPage(DETECTIONS_URL);
loginAndWaitForPage(ALERTS_URL);
waitForAlertsPanelToBeLoaded();
waitForAlertsIndexToBeCreated();
createCustomRuleActivated(newRule, '1', '100m', 100);

View file

@ -28,12 +28,12 @@ import { waitForAlertsToPopulate } from '../../tasks/create_new_rule';
import { loginAndWaitForPage } from '../../tasks/login';
import { refreshPage } from '../../tasks/security_header';
import { DETECTIONS_URL } from '../../urls/navigation';
import { ALERTS_URL } from '../../urls/navigation';
describe('Marking alerts as in-progress', () => {
beforeEach(() => {
cleanKibana();
loginAndWaitForPage(DETECTIONS_URL);
loginAndWaitForPage(ALERTS_URL);
waitForAlertsPanelToBeLoaded();
waitForAlertsIndexToBeCreated();
createCustomRuleActivated(newRule);

View file

@ -19,12 +19,12 @@ import { waitForAlertsToPopulate } from '../../tasks/create_new_rule';
import { loginAndWaitForPage } from '../../tasks/login';
import { refreshPage } from '../../tasks/security_header';
import { DETECTIONS_URL } from '../../urls/navigation';
import { ALERTS_URL } from '../../urls/navigation';
describe('Alerts timeline', () => {
beforeEach(() => {
cleanKibana();
loginAndWaitForPage(DETECTIONS_URL);
loginAndWaitForPage(ALERTS_URL);
waitForAlertsPanelToBeLoaded();
waitForAlertsIndexToBeCreated();
createCustomRuleActivated(newRule);

View file

@ -6,7 +6,7 @@
*/
import { ROLES } from '../../../common/test';
import { DETECTIONS_RULE_MANAGEMENT_URL, DETECTIONS_URL } from '../../urls/navigation';
import { DETECTIONS_RULE_MANAGEMENT_URL, ALERTS_URL } from '../../urls/navigation';
import { newRule } from '../../objects/rule';
import { PAGE_TITLE } from '../../screens/common/page';
@ -47,7 +47,7 @@ describe('Detections > Callouts', () => {
// First, we have to open the app on behalf of a privileged user in order to initialize it.
// Otherwise the app will be disabled and show a "welcome"-like page.
cleanKibana();
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL, ROLES.platform_engineer);
loginAndWaitForPageWithoutDateRange(ALERTS_URL, ROLES.platform_engineer);
waitForAlertsIndexToBeCreated();
// After that we can login as a read-only user.
@ -57,7 +57,7 @@ describe('Detections > Callouts', () => {
context('indicating read-only access to resources', () => {
context('On Detections home page', () => {
beforeEach(() => {
loadPageAsReadOnlyUser(DETECTIONS_URL);
loadPageAsReadOnlyUser(ALERTS_URL);
});
it('We show one primary callout', () => {
@ -125,7 +125,7 @@ describe('Detections > Callouts', () => {
context('indicating read-write access to resources', () => {
context('On Detections home page', () => {
beforeEach(() => {
loadPageAsPlatformEngineer(DETECTIONS_URL);
loadPageAsPlatformEngineer(ALERTS_URL);
});
it('We show no callout', () => {

View file

@ -29,12 +29,12 @@ import { waitForAlertsToPopulate } from '../../tasks/create_new_rule';
import { loginAndWaitForPage } from '../../tasks/login';
import { refreshPage } from '../../tasks/security_header';
import { DETECTIONS_URL } from '../../urls/navigation';
import { ALERTS_URL } from '../../urls/navigation';
describe('Opening alerts', () => {
beforeEach(() => {
cleanKibana();
loginAndWaitForPage(DETECTIONS_URL);
loginAndWaitForPage(ALERTS_URL);
waitForAlertsPanelToBeLoaded();
waitForAlertsIndexToBeCreated();
createCustomRuleActivated(newRule);

View file

@ -110,7 +110,7 @@ import { saveEditedRule, waitForKibana } from '../../tasks/edit_rule';
import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import { activatesRule } from '../../tasks/rule_details';
import { DETECTIONS_URL } from '../../urls/navigation';
import { ALERTS_URL } from '../../urls/navigation';
describe('Custom detection rules creation', () => {
const expectedUrls = newRule.referenceUrls.join('');
@ -133,7 +133,7 @@ describe('Custom detection rules creation', () => {
});
it('Creates and activates a new rule', function () {
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
loginAndWaitForPageWithoutDateRange(ALERTS_URL);
waitForAlertsPanelToBeLoaded();
waitForAlertsIndexToBeCreated();
goToManageAlertsDetectionRules();
@ -226,7 +226,7 @@ describe('Custom detection rules deletion and edition', () => {
context('Deletion', () => {
beforeEach(() => {
cleanKibana();
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
loginAndWaitForPageWithoutDateRange(ALERTS_URL);
goToManageAlertsDetectionRules();
waitForAlertsIndexToBeCreated();
createCustomRuleActivated(newRule, 'rule1');
@ -302,7 +302,7 @@ describe('Custom detection rules deletion and edition', () => {
beforeEach(() => {
cleanKibana();
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
loginAndWaitForPageWithoutDateRange(ALERTS_URL);
goToManageAlertsDetectionRules();
waitForAlertsIndexToBeCreated();
createCustomRuleActivated(existingRule, 'rule1');

View file

@ -75,7 +75,7 @@ import {
} from '../../tasks/create_new_rule';
import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import { DETECTIONS_URL } from '../../urls/navigation';
import { ALERTS_URL } from '../../urls/navigation';
describe('Detection rules, EQL', () => {
const expectedUrls = eqlRule.referenceUrls.join('');
@ -99,7 +99,7 @@ describe('Detection rules, EQL', () => {
});
it('Creates and activates a new EQL rule', function () {
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
loginAndWaitForPageWithoutDateRange(ALERTS_URL);
waitForAlertsPanelToBeLoaded();
waitForAlertsIndexToBeCreated();
goToManageAlertsDetectionRules();
@ -194,7 +194,7 @@ describe('Detection rules, sequence EQL', () => {
});
it('Creates and activates a new EQL rule with a sequence', function () {
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
loginAndWaitForPageWithoutDateRange(ALERTS_URL);
waitForAlertsPanelToBeLoaded();
waitForAlertsIndexToBeCreated();
goToManageAlertsDetectionRules();

View file

@ -16,7 +16,7 @@ import { createCustomRule } from '../../tasks/api_calls/rules';
import { cleanKibana } from '../../tasks/common';
import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import { DETECTIONS_URL } from '../../urls/navigation';
import { ALERTS_URL } from '../../urls/navigation';
describe('Export rules', () => {
beforeEach(() => {
@ -25,7 +25,7 @@ describe('Export rules', () => {
'POST',
'/api/detection_engine/rules/_export?exclude_export_details=false&file_name=rules_export.ndjson'
).as('export');
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
loginAndWaitForPageWithoutDateRange(ALERTS_URL);
waitForAlertsPanelToBeLoaded();
waitForAlertsIndexToBeCreated();
createCustomRule(newRule).as('ruleResponse');

View file

@ -123,7 +123,7 @@ import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver';
import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import { addsFieldsToTimeline, goBackToAllRulesTable } from '../../tasks/rule_details';
import { DETECTIONS_URL, RULE_CREATION } from '../../urls/navigation';
import { ALERTS_URL, RULE_CREATION } from '../../urls/navigation';
describe('indicator match', () => {
describe('Detection rules, Indicator Match', () => {
@ -407,7 +407,7 @@ describe('indicator match', () => {
describe('Generating signals', () => {
beforeEach(() => {
cleanKibana();
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
loginAndWaitForPageWithoutDateRange(ALERTS_URL);
});
it('Creates and activates a new Indicator Match rule', () => {
@ -559,7 +559,7 @@ describe('indicator match', () => {
cleanKibana();
esArchiverLoad('threat_indicator');
esArchiverLoad('suspicious_source_event');
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
loginAndWaitForPageWithoutDateRange(ALERTS_URL);
goToManageAlertsDetectionRules();
createCustomIndicatorRule(newThreatIndicatorRule);
reload();
@ -571,7 +571,7 @@ describe('indicator match', () => {
});
beforeEach(() => {
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
loginAndWaitForPageWithoutDateRange(ALERTS_URL);
goToManageAlertsDetectionRules();
goToRuleDetails();
});
@ -687,7 +687,7 @@ describe('indicator match', () => {
describe('Duplicates the indicator rule', () => {
beforeEach(() => {
cleanKibana();
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
loginAndWaitForPageWithoutDateRange(ALERTS_URL);
goToManageAlertsDetectionRules();
createCustomIndicatorRule(newThreatIndicatorRule);
reload();

View file

@ -62,7 +62,7 @@ import {
} from '../../tasks/create_new_rule';
import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import { DETECTIONS_URL } from '../../urls/navigation';
import { ALERTS_URL } from '../../urls/navigation';
describe('Detection rules, machine learning', () => {
const expectedUrls = machineLearningRule.referenceUrls.join('');
@ -76,7 +76,7 @@ describe('Detection rules, machine learning', () => {
});
it('Creates and activates a new ml rule', () => {
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
loginAndWaitForPageWithoutDateRange(ALERTS_URL);
waitForAlertsPanelToBeLoaded();
waitForAlertsIndexToBeCreated();
goToManageAlertsDetectionRules();

View file

@ -86,7 +86,7 @@ import {
} from '../../tasks/create_new_rule';
import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import { DETECTIONS_URL } from '../../urls/navigation';
import { ALERTS_URL } from '../../urls/navigation';
describe('Detection rules, override', () => {
const expectedUrls = newOverrideRule.referenceUrls.join('');
@ -108,7 +108,7 @@ describe('Detection rules, override', () => {
});
it('Creates and activates a new custom rule with override option', function () {
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
loginAndWaitForPageWithoutDateRange(ALERTS_URL);
waitForAlertsPanelToBeLoaded();
waitForAlertsIndexToBeCreated();
goToManageAlertsDetectionRules();

View file

@ -34,7 +34,7 @@ import {
} from '../../tasks/alerts_detection_rules';
import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import { DETECTIONS_URL } from '../../urls/navigation';
import { ALERTS_URL } from '../../urls/navigation';
import { totalNumberOfPrebuiltRules } from '../../objects/rule';
import { cleanKibana } from '../../tasks/common';
@ -50,7 +50,7 @@ describe('Alerts rules, prebuilt rules', () => {
const expectedNumberOfPages = Math.ceil(totalNumberOfPrebuiltRules / rowsPerPage);
const expectedElasticRulesBtnText = `Elastic rules (${expectedNumberOfRules})`;
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
loginAndWaitForPageWithoutDateRange(ALERTS_URL);
waitForAlertsIndexToBeCreated();
goToManageAlertsDetectionRules();
waitForRulesTableToBeLoaded();
@ -72,7 +72,7 @@ describe('Actions with prebuilt rules', () => {
const expectedElasticRulesBtnText = `Elastic rules (${expectedNumberOfRules})`;
cleanKibana();
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
loginAndWaitForPageWithoutDateRange(ALERTS_URL);
waitForAlertsIndexToBeCreated();
goToManageAlertsDetectionRules();
waitForRulesTableToBeLoaded();

View file

@ -36,7 +36,7 @@ import {
import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import { DEFAULT_RULE_REFRESH_INTERVAL_VALUE } from '../../../common/constants';
import { DETECTIONS_URL } from '../../urls/navigation';
import { ALERTS_URL } from '../../urls/navigation';
import { createCustomRule } from '../../tasks/api_calls/rules';
import { cleanKibana } from '../../tasks/common';
import { existingRule, newOverrideRule, newRule, newThresholdRule } from '../../objects/rule';
@ -44,7 +44,7 @@ import { existingRule, newOverrideRule, newRule, newThresholdRule } from '../../
describe('Alerts detection rules', () => {
beforeEach(() => {
cleanKibana();
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
loginAndWaitForPageWithoutDateRange(ALERTS_URL);
waitForAlertsPanelToBeLoaded();
waitForAlertsIndexToBeCreated();
createCustomRule(newRule, '1');

View file

@ -81,7 +81,7 @@ import {
} from '../../tasks/create_new_rule';
import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import { DETECTIONS_URL } from '../../urls/navigation';
import { ALERTS_URL } from '../../urls/navigation';
describe('Detection rules, threshold', () => {
const expectedUrls = newThresholdRule.referenceUrls.join('');
@ -96,7 +96,7 @@ describe('Detection rules, threshold', () => {
createTimeline(newThresholdRule.timeline).then((response) => {
rule.timeline.id = response.body.data.persistTimeline.timeline.savedObjectId;
});
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
loginAndWaitForPageWithoutDateRange(ALERTS_URL);
waitForAlertsPanelToBeLoaded();
waitForAlertsIndexToBeCreated();
});

View file

@ -31,7 +31,7 @@ import {
ADD_EXCEPTIONS_BTN,
} from '../../screens/exceptions';
import { DETECTIONS_URL } from '../../urls/navigation';
import { ALERTS_URL } from '../../urls/navigation';
import { cleanKibana } from '../../tasks/common';
// NOTE: You might look at these tests and feel they're overkill,
@ -42,7 +42,7 @@ import { cleanKibana } from '../../tasks/common';
describe('Exceptions modal', () => {
before(() => {
cleanKibana();
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
loginAndWaitForPageWithoutDateRange(ALERTS_URL);
waitForAlertsIndexToBeCreated();
createCustomRule(newRule);
goToManageAlertsDetectionRules();

View file

@ -14,7 +14,10 @@ import { goToManageAlertsDetectionRules, waitForAlertsIndexToBeCreated } from '.
import { createCustomRule } from '../../tasks/api_calls/rules';
import { goToRuleDetails, waitForRulesTableToBeLoaded } from '../../tasks/alerts_detection_rules';
import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver';
import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import {
loginAndWaitForPageWithoutDateRange,
waitForPageWithoutDateRange,
} from '../../tasks/login';
import {
addsExceptionFromRuleSettings,
goBackToAllRulesTable,
@ -22,13 +25,12 @@ import {
waitForTheRuleToBeExecuted,
} from '../../tasks/rule_details';
import { DETECTIONS_URL } from '../../urls/navigation';
import { ALERTS_URL, EXCEPTIONS_URL } from '../../urls/navigation';
import { cleanKibana } from '../../tasks/common';
import {
deleteExceptionListWithRuleReference,
deleteExceptionListWithoutRuleReference,
exportExceptionList,
goToExceptionsTable,
searchForExceptionList,
waitForExceptionsTableToBeLoaded,
clearSearchSelection,
@ -42,7 +44,7 @@ import { createExceptionList } from '../../tasks/api_calls/exceptions';
describe('Exceptions Table', () => {
before(() => {
cleanKibana();
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
loginAndWaitForPageWithoutDateRange(ALERTS_URL);
waitForAlertsIndexToBeCreated();
createCustomRule(newRule);
goToManageAlertsDetectionRules();
@ -69,7 +71,7 @@ describe('Exceptions Table', () => {
});
it('Filters exception lists on search', () => {
goToExceptionsTable();
waitForPageWithoutDateRange(EXCEPTIONS_URL);
waitForExceptionsTableToBeLoaded();
cy.get(EXCEPTIONS_TABLE_SHOWING_LISTS).should('have.text', `Showing 3 lists`);
@ -110,7 +112,7 @@ describe('Exceptions Table', () => {
it('Exports exception list', async function () {
cy.intercept(/(\/api\/exception_lists\/_export)/).as('export');
goToExceptionsTable();
waitForPageWithoutDateRange(EXCEPTIONS_URL);
waitForExceptionsTableToBeLoaded();
exportExceptionList();
@ -124,7 +126,7 @@ describe('Exceptions Table', () => {
});
it('Deletes exception list without rule reference', () => {
goToExceptionsTable();
waitForPageWithoutDateRange(EXCEPTIONS_URL);
waitForExceptionsTableToBeLoaded();
cy.get(EXCEPTIONS_TABLE_SHOWING_LISTS).should('have.text', `Showing 3 lists`);
@ -135,7 +137,7 @@ describe('Exceptions Table', () => {
});
it('Deletes exception list with rule reference', () => {
goToExceptionsTable();
waitForPageWithoutDateRange(EXCEPTIONS_URL);
waitForExceptionsTableToBeLoaded();
cy.get(EXCEPTIONS_TABLE_SHOWING_LISTS).should('have.text', `Showing 2 lists`);

View file

@ -33,7 +33,7 @@ import {
} from '../../tasks/rule_details';
import { refreshPage } from '../../tasks/security_header';
import { DETECTIONS_URL } from '../../urls/navigation';
import { ALERTS_URL } from '../../urls/navigation';
import { cleanKibana } from '../../tasks/common';
describe('From alert', () => {
@ -41,7 +41,7 @@ describe('From alert', () => {
beforeEach(() => {
cleanKibana();
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
loginAndWaitForPageWithoutDateRange(ALERTS_URL);
waitForAlertsIndexToBeCreated();
createCustomRule(newRule, 'rule_testing', '10s');
goToManageAlertsDetectionRules();

View file

@ -32,14 +32,14 @@ import {
} from '../../tasks/rule_details';
import { refreshPage } from '../../tasks/security_header';
import { DETECTIONS_URL } from '../../urls/navigation';
import { ALERTS_URL } from '../../urls/navigation';
import { cleanKibana } from '../../tasks/common';
describe('From rule', () => {
const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1';
beforeEach(() => {
cleanKibana();
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
loginAndWaitForPageWithoutDateRange(ALERTS_URL);
waitForAlertsIndexToBeCreated();
createCustomRule(newRule, 'rule_testing', '10s');
goToManageAlertsDetectionRules();

View file

@ -9,7 +9,9 @@ import {
CASES,
DETECTIONS,
HOSTS,
ADMINISTRATION,
ENDPOINTS,
TRUSTED_APPS,
EVENT_FILTERS,
NETWORK,
OVERVIEW,
TIMELINES,
@ -19,11 +21,13 @@ import { loginAndWaitForPage } from '../../tasks/login';
import { navigateFromHeaderTo } from '../../tasks/security_header';
import {
DETECTIONS_URL,
ALERTS_URL,
CASES_URL,
HOSTS_URL,
KIBANA_HOME,
ADMINISTRATION_URL,
ENDPOINTS_URL,
TRUSTED_APPS_URL,
EVENT_FILTERS_URL,
NETWORK_URL,
OVERVIEW_URL,
TIMELINES_URL,
@ -34,9 +38,9 @@ import {
} from '../../tasks/kibana_navigation';
import {
CASES_PAGE,
DETECTIONS_PAGE,
ALERTS_PAGE,
HOSTS_PAGE,
ADMINISTRATION_PAGE,
ENDPOINTS_PAGE,
NETWORK_PAGE,
OVERVIEW_PAGE,
TIMELINES_PAGE,
@ -54,9 +58,9 @@ describe('top-level navigation common to all pages in the Security app', () => {
cy.url().should('include', OVERVIEW_URL);
});
it('navigates to the Detections page', () => {
it('navigates to the Alerts page', () => {
navigateFromHeaderTo(DETECTIONS);
cy.url().should('include', DETECTIONS_URL);
cy.url().should('include', ALERTS_URL);
});
it('navigates to the Hosts page', () => {
@ -79,9 +83,17 @@ describe('top-level navigation common to all pages in the Security app', () => {
cy.url().should('include', CASES_URL);
});
it('navigates to the Administration page', () => {
navigateFromHeaderTo(ADMINISTRATION);
cy.url().should('include', ADMINISTRATION_URL);
it('navigates to the Endpoints page', () => {
navigateFromHeaderTo(ENDPOINTS);
cy.url().should('include', ENDPOINTS_URL);
});
it('navigates to the Trusted Apps page', () => {
navigateFromHeaderTo(TRUSTED_APPS);
cy.url().should('include', TRUSTED_APPS_URL);
});
it('navigates to the Event Filters page', () => {
navigateFromHeaderTo(EVENT_FILTERS);
cy.url().should('include', EVENT_FILTERS_URL);
});
});
@ -97,9 +109,9 @@ describe('Kibana navigation to all pages in the Security app ', () => {
cy.url().should('include', OVERVIEW_URL);
});
it('navigates to the Detections page', () => {
navigateFromKibanaCollapsibleTo(DETECTIONS_PAGE);
cy.url().should('include', DETECTIONS_URL);
it('navigates to the Alerts page', () => {
navigateFromKibanaCollapsibleTo(ALERTS_PAGE);
cy.url().should('include', ALERTS_URL);
});
it('navigates to the Hosts page', () => {
@ -122,8 +134,8 @@ describe('Kibana navigation to all pages in the Security app ', () => {
cy.url().should('include', CASES_URL);
});
it('navigates to the Administration page', () => {
navigateFromKibanaCollapsibleTo(ADMINISTRATION_PAGE);
cy.url().should('include', ADMINISTRATION_URL);
it('navigates to the Endpoints page', () => {
navigateFromKibanaCollapsibleTo(ENDPOINTS_PAGE);
cy.url().should('include', ENDPOINTS_URL);
});
});

View file

@ -7,7 +7,15 @@
import { loginAndWaitForPage, loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import { DETECTIONS } from '../../urls/navigation';
import {
ALERTS_URL,
DETECTIONS,
DETECTIONS_RULE_MANAGEMENT_URL,
RULE_CREATION,
SECURITY_DETECTIONS_RULES_CREATION_URL,
SECURITY_DETECTIONS_RULES_URL,
SECURITY_DETECTIONS_URL,
} from '../../urls/navigation';
import { ABSOLUTE_DATE_RANGE } from '../../urls/state';
import {
DATE_PICKER_START_DATE_POPOVER_BUTTON,
@ -25,10 +33,24 @@ describe('URL compatibility', () => {
cleanKibana();
});
it('Redirects to Detection alerts from old Detections URL', () => {
it('Redirects to alerts from old siem Detections URL', () => {
loginAndWaitForPage(DETECTIONS);
cy.url().should('include', ALERTS_URL);
});
cy.url().should('include', '/security/detections');
it('Redirects to alerts from old Detections URL', () => {
loginAndWaitForPage(SECURITY_DETECTIONS_URL);
cy.url().should('include', ALERTS_URL);
});
it('Redirects to rules from old Detections rules URL', () => {
loginAndWaitForPage(SECURITY_DETECTIONS_RULES_URL);
cy.url().should('include', DETECTIONS_RULE_MANAGEMENT_URL);
});
it('Redirects to rules creation from old Detections rules creation URL', () => {
loginAndWaitForPage(SECURITY_DETECTIONS_RULES_CREATION_URL);
cy.url().should('include', RULE_CREATION);
});
it('sets the global start and end dates from the url with timestamps', () => {

View file

@ -214,7 +214,7 @@ describe('url state', () => {
cy.get(ANOMALIES_TAB).should(
'have.attr',
'href',
"/app/security/hosts/siem-kibana/anomalies?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&sourcerer=(default:!('auditbeat-*'))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))"
"/app/security/hosts/siem-kibana/anomalies?sourcerer=(default:!('auditbeat-*'))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))&query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')"
);
cy.get(BREADCRUMBS)
.eq(1)

View file

@ -7,7 +7,7 @@
import { ROLES } from '../../../common/test';
import { deleteRoleAndUser, loginAndWaitForPageWithoutDateRange } from '../../tasks/login';
import { DETECTIONS_URL } from '../../urls/navigation';
import { ALERTS_URL } from '../../urls/navigation';
import {
waitForAlertsPanelToBeLoaded,
waitForAlertsIndexToBeCreated,
@ -35,7 +35,7 @@ import {
describe('value lists', () => {
describe('management modal', () => {
beforeEach(() => {
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL);
loginAndWaitForPageWithoutDateRange(ALERTS_URL);
waitForAlertsPanelToBeLoaded();
waitForAlertsIndexToBeCreated();
waitForListsIndexToBeCreated();
@ -228,7 +228,7 @@ describe('value lists', () => {
describe('user with restricted access role', () => {
beforeEach(() => {
loginAndWaitForPageWithoutDateRange(DETECTIONS_URL, ROLES.t1_analyst);
loginAndWaitForPageWithoutDateRange(ALERTS_URL, ROLES.t1_analyst);
goToManageAlertsDetectionRules();
});

View file

@ -42,7 +42,7 @@ export const EXCEPTIONS_TABLE_TAB = '[data-test-subj="allRulesTableTab-exception
export const EXCEPTIONS_TABLE = '[data-test-subj="exceptions-table"]';
export const EXCEPTIONS_TABLE_SEARCH = '[data-test-subj="header-section-supplements"] input';
export const EXCEPTIONS_TABLE_SEARCH = '[data-test-subj="exceptionsHeaderSearchInput"]';
export const EXCEPTIONS_TABLE_SHOWING_LISTS = '[data-test-subj="showingExceptionLists"]';
@ -50,7 +50,8 @@ export const EXCEPTIONS_TABLE_DELETE_BTN = '[data-test-subj="exceptionsTableDele
export const EXCEPTIONS_TABLE_EXPORT_BTN = '[data-test-subj="exceptionsTableExportButton"]';
export const EXCEPTIONS_TABLE_SEARCH_CLEAR = '[data-test-subj="header-section-supplements"] button';
export const EXCEPTIONS_TABLE_SEARCH_CLEAR =
'[data-test-subj="allExceptionListsPanel"] button.euiFormControlLayoutClearButton';
export const EXCEPTIONS_TABLE_LIST_NAME = '[data-test-subj="exceptionsTableName"]';

View file

@ -5,8 +5,8 @@
* 2.0.
*/
export const DETECTIONS_PAGE =
'[data-test-subj="collapsibleNavGroup-securitySolution"] [title="Detections"]';
export const ALERTS_PAGE =
'[data-test-subj="collapsibleNavGroup-securitySolution"] [title="Alerts"]';
export const CASES_PAGE = '[data-test-subj="collapsibleNavGroup-securitySolution"] [title="Cases"]';
@ -14,8 +14,8 @@ export const HOSTS_PAGE = '[data-test-subj="collapsibleNavGroup-securitySolution
export const KIBANA_NAVIGATION_TOGGLE = '[data-test-subj="toggleNavButton"]';
export const ADMINISTRATION_PAGE =
'[data-test-subj="collapsibleNavGroup-securitySolution"] [title="Administration"]';
export const ENDPOINTS_PAGE =
'[data-test-subj="collapsibleNavGroup-securitySolution"] [title="Endpoints"]';
export const NETWORK_PAGE =
'[data-test-subj="collapsibleNavGroup-securitySolution"] [title="Network"]';

View file

@ -5,7 +5,7 @@
* 2.0.
*/
export const DETECTIONS = '[data-test-subj="navigation-detections"]';
export const DETECTIONS = '[data-test-subj="navigation-alerts"]';
export const BREADCRUMBS = '[data-test-subj="breadcrumbs"] a';
@ -15,7 +15,11 @@ export const HOSTS = '[data-test-subj="navigation-hosts"]';
export const KQL_INPUT = '[data-test-subj="queryInput"]';
export const ADMINISTRATION = '[data-test-subj="navigation-administration"]';
export const ENDPOINTS = '[data-test-subj="navigation-endpoints"]';
export const TRUSTED_APPS = '[data-test-subj="navigation-trusted_apps"]';
export const EVENT_FILTERS = '[data-test-subj="navigation-event_filters"]';
export const NETWORK = '[data-test-subj="navigation-network"]';

View file

@ -5,13 +5,18 @@
* 2.0.
*/
export const DETECTIONS_URL = 'app/security/detections';
export const DETECTIONS_RULE_MANAGEMENT_URL = 'app/security/detections/rules';
export const detectionsRuleDetailsUrl = (ruleId: string) =>
`app/security/detections/rules/id/${ruleId}`;
export const ALERTS_URL = 'app/security/alerts';
export const DETECTIONS_RULE_MANAGEMENT_URL = 'app/security/rules';
export const detectionsRuleDetailsUrl = (ruleId: string) => `app/security/rules/id/${ruleId}`;
export const CASES_URL = '/app/security/cases';
export const DETECTIONS = '/app/siem#/detections';
export const SECURITY_DETECTIONS_URL = '/app/security/detections';
export const SECURITY_DETECTIONS_RULES_URL = '/app/security/detections/rules';
export const SECURITY_DETECTIONS_RULES_CREATION_URL = '/app/security/detections/rules/create';
export const EXCEPTIONS_URL = 'app/security/exceptions';
export const HOSTS_URL = '/app/security/hosts/allHosts';
export const HOSTS_PAGE_TAB_URLS = {
allHosts: '/app/security/hosts/allHosts',
@ -21,9 +26,11 @@ export const HOSTS_PAGE_TAB_URLS = {
uncommonProcesses: '/app/security/hosts/uncommonProcesses',
};
export const KIBANA_HOME = '/app/home#/';
export const ADMINISTRATION_URL = '/app/security/administration';
export const ENDPOINTS_URL = '/app/security/administration/endpoints';
export const TRUSTED_APPS_URL = '/app/security/administration/trusted_apps';
export const EVENT_FILTERS_URL = '/app/security/administration/event_filters';
export const NETWORK_URL = '/app/security/network';
export const OVERVIEW_URL = '/app/security/overview';
export const RULE_CREATION = 'app/security/detections/rules/create';
export const RULE_CREATION = 'app/security/rules/create';
export const TIMELINES_URL = '/app/security/timelines';
export const TIMELINE_TEMPLATES_URL = '/app/security/timelines/template';

View file

@ -37,5 +37,13 @@
],
"server": true,
"ui": true,
"requiredBundles": ["esUiShared", "fleet", "kibanaUtils", "kibanaReact", "lists", "ml"]
"requiredBundles": [
"esUiShared",
"fleet",
"kibanaUtils",
"kibanaReact",
"usageCollection",
"lists",
"ml"
]
}

View file

@ -0,0 +1,82 @@
/*
* 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 { getDeepLinks } from '.';
import { Capabilities } from '../../../../../../src/core/public';
import { SecurityPageName } from '../types';
describe('public search functions', () => {
it('returns a subset of links for basic license, full set for platinum', () => {
const basicLicense = 'basic';
const platinumLicense = 'platinum';
const basicLinks = getDeepLinks(basicLicense);
const platinumLinks = getDeepLinks(platinumLicense);
basicLinks.forEach((basicLink, index) => {
const platinumLink = platinumLinks[index];
expect(basicLink.id).toEqual(platinumLink.id);
const platinumDeepLinksCount = platinumLink.deepLinks?.length || 0;
const basicDeepLinksCount = basicLink.deepLinks?.length || 0;
expect(platinumDeepLinksCount).toBeGreaterThanOrEqual(basicDeepLinksCount);
});
});
it('returns case links for basic license with only read_cases capabilities', () => {
const basicLicense = 'basic';
const basicLinks = getDeepLinks(basicLicense, ({
siem: { read_cases: true, crud_cases: false },
} as unknown) as Capabilities);
expect(basicLinks.some((l) => l.id === SecurityPageName.case)).toBeTruthy();
});
it('returns case links with NO deepLinks for basic license with only read_cases capabilities', () => {
const basicLicense = 'basic';
const basicLinks = getDeepLinks(basicLicense, ({
siem: { read_cases: true, crud_cases: false },
} as unknown) as Capabilities);
expect(
basicLinks.find((l) => l.id === SecurityPageName.case)?.deepLinks?.length === 0
).toBeTruthy();
});
it('returns case links with deepLinks for basic license with crud_cases capabilities', () => {
const basicLicense = 'basic';
const basicLinks = getDeepLinks(basicLicense, ({
siem: { read_cases: true, crud_cases: true },
} as unknown) as Capabilities);
expect(
(basicLinks.find((l) => l.id === SecurityPageName.case)?.deepLinks?.length ?? 0) > 0
).toBeTruthy();
});
it('returns NO case links for basic license with NO read_cases capabilities', () => {
const basicLicense = 'basic';
const basicLinks = getDeepLinks(basicLicense, ({
siem: { read_cases: false, crud_cases: false },
} as unknown) as Capabilities);
expect(basicLinks.some((l) => l.id === SecurityPageName.case)).toBeFalsy();
});
it('returns case links for basic license with undefined capabilities', () => {
const basicLicense = 'basic';
const basicLinks = getDeepLinks(basicLicense, undefined);
expect(basicLinks.some((l) => l.id === SecurityPageName.case)).toBeTruthy();
});
it('returns case deepLinks for basic license with undefined capabilities', () => {
const basicLicense = 'basic';
const basicLinks = getDeepLinks(basicLicense, undefined);
expect(
(basicLinks.find((l) => l.id === SecurityPageName.case)?.deepLinks?.length ?? 0) > 0
).toBeTruthy();
});
});

View file

@ -0,0 +1,395 @@
/*
* 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 { Subject } from 'rxjs';
import { LicenseType } from '../../../../licensing/common/types';
import { SecurityDeepLinkName, SecurityDeepLinks, SecurityPageName } from '../types';
import {
AppDeepLink,
ApplicationStart,
AppNavLinkStatus,
AppUpdater,
} from '../../../../../../src/core/public';
import {
OVERVIEW,
DETECT,
ALERTS,
RULES,
EXCEPTIONS,
HOSTS,
NETWORK,
TIMELINES,
CASE,
ADMINISTRATION,
} from '../translations';
import {
OVERVIEW_PATH,
ALERTS_PATH,
RULES_PATH,
EXCEPTIONS_PATH,
HOSTS_PATH,
NETWORK_PATH,
TIMELINES_PATH,
CASES_PATH,
ENDPOINTS_PATH,
TRUSTED_APPS_PATH,
EVENT_FILTERS_PATH,
} from '../../../common/constants';
export const topDeepLinks: AppDeepLink[] = [
{
id: SecurityPageName.overview,
title: OVERVIEW,
path: OVERVIEW_PATH,
navLinkStatus: AppNavLinkStatus.visible,
keywords: [
i18n.translate('xpack.securitySolution.search.overview', {
defaultMessage: 'Overview',
}),
],
order: 9000,
},
{
id: SecurityPageName.detections,
title: DETECT,
path: ALERTS_PATH,
navLinkStatus: AppNavLinkStatus.hidden,
keywords: [
i18n.translate('xpack.securitySolution.search.detect', {
defaultMessage: 'Detect',
}),
],
},
{
id: SecurityPageName.hosts,
title: HOSTS,
path: HOSTS_PATH,
navLinkStatus: AppNavLinkStatus.visible,
keywords: [
i18n.translate('xpack.securitySolution.search.hosts', {
defaultMessage: 'Hosts',
}),
],
order: 9002,
},
{
id: SecurityPageName.network,
title: NETWORK,
path: NETWORK_PATH,
navLinkStatus: AppNavLinkStatus.visible,
keywords: [
i18n.translate('xpack.securitySolution.search.network', {
defaultMessage: 'Network',
}),
],
order: 9003,
},
{
id: SecurityPageName.timelines,
title: TIMELINES,
path: TIMELINES_PATH,
navLinkStatus: AppNavLinkStatus.visible,
keywords: [
i18n.translate('xpack.securitySolution.search.timelines', {
defaultMessage: 'Timelines',
}),
],
order: 9004,
},
{
id: SecurityPageName.case,
title: CASE,
path: CASES_PATH,
navLinkStatus: AppNavLinkStatus.visible,
keywords: [
i18n.translate('xpack.securitySolution.search.cases', {
defaultMessage: 'Cases',
}),
],
order: 9005,
},
{
id: SecurityPageName.administration,
title: ADMINISTRATION,
path: ENDPOINTS_PATH,
navLinkStatus: AppNavLinkStatus.hidden,
keywords: [
i18n.translate('xpack.securitySolution.search.administration', {
defaultMessage: 'Administration',
}),
],
},
];
const nestedDeepLinks: SecurityDeepLinks = {
[SecurityPageName.overview]: {
base: [],
},
[SecurityPageName.detections]: {
base: [
{
id: SecurityPageName.alerts,
title: ALERTS,
path: ALERTS_PATH,
navLinkStatus: AppNavLinkStatus.visible,
keywords: [
i18n.translate('xpack.securitySolution.search.alerts', {
defaultMessage: 'Alerts',
}),
],
searchable: true,
order: 9001,
},
{
id: SecurityPageName.rules,
title: RULES,
path: RULES_PATH,
navLinkStatus: AppNavLinkStatus.hidden,
keywords: [
i18n.translate('xpack.securitySolution.search.rules', {
defaultMessage: 'Rules',
}),
],
searchable: true,
},
{
id: SecurityPageName.exceptions,
title: EXCEPTIONS,
path: EXCEPTIONS_PATH,
navLinkStatus: AppNavLinkStatus.hidden,
keywords: [
i18n.translate('xpack.securitySolution.search.exceptions', {
defaultMessage: 'Exceptions',
}),
],
searchable: true,
},
],
},
[SecurityPageName.hosts]: {
base: [
{
id: 'authentications',
title: i18n.translate('xpack.securitySolution.search.hosts.authentications', {
defaultMessage: 'Authentications',
}),
path: '/authentications',
},
{
id: 'uncommonProcesses',
title: i18n.translate('xpack.securitySolution.search.hosts.uncommonProcesses', {
defaultMessage: 'Uncommon Processes',
}),
path: '/uncommonProcesses',
},
{
id: 'events',
title: i18n.translate('xpack.securitySolution.search.hosts.events', {
defaultMessage: 'Events',
}),
path: '/events',
},
{
id: 'externalAlerts',
title: i18n.translate('xpack.securitySolution.search.hosts.externalAlerts', {
defaultMessage: 'External Alerts',
}),
path: '/alerts',
},
],
premium: [
{
id: 'anomalies',
title: i18n.translate('xpack.securitySolution.search.hosts.anomalies', {
defaultMessage: 'Anomalies',
}),
path: '/anomalies',
},
],
},
[SecurityPageName.network]: {
base: [
{
id: 'dns',
title: i18n.translate('xpack.securitySolution.search.network.dns', {
defaultMessage: 'DNS',
}),
path: '/dns',
},
{
id: 'http',
title: i18n.translate('xpack.securitySolution.search.network.http', {
defaultMessage: 'HTTP',
}),
path: '/http',
},
{
id: 'tls',
title: i18n.translate('xpack.securitySolution.search.network.tls', {
defaultMessage: 'TLS',
}),
path: '/tls',
},
{
id: 'externalAlertsNetwork',
title: i18n.translate('xpack.securitySolution.search.network.externalAlerts', {
defaultMessage: 'External Alerts',
}),
path: '/external-alerts',
},
],
premium: [
{
id: 'anomalies',
title: i18n.translate('xpack.securitySolution.search.hosts.anomalies', {
defaultMessage: 'Anomalies',
}),
path: '/anomalies',
},
],
},
[SecurityPageName.timelines]: {
base: [
{
id: 'timelineTemplates',
title: i18n.translate('xpack.securitySolution.search.timeline.templates', {
defaultMessage: 'Templates',
}),
path: '/template',
},
],
},
[SecurityPageName.case]: {
base: [
{
id: 'create',
title: i18n.translate('xpack.securitySolution.search.cases.create', {
defaultMessage: 'Create New Case',
}),
path: '/create',
},
],
premium: [
{
id: 'configure',
title: i18n.translate('xpack.securitySolution.search.cases.configure', {
defaultMessage: 'Configure Cases',
}),
path: '/configure',
},
],
},
[SecurityPageName.administration]: {
base: [
{
id: SecurityPageName.endpoints,
navLinkStatus: AppNavLinkStatus.visible,
title: i18n.translate('xpack.securitySolution.search.administration.endpoints', {
defaultMessage: 'Endpoints',
}),
order: 9006,
path: ENDPOINTS_PATH,
},
{
id: SecurityPageName.trustedApps,
title: i18n.translate('xpack.securitySolution.search.administration.trustedApps', {
defaultMessage: 'Trusted Applications',
}),
path: TRUSTED_APPS_PATH,
},
{
id: SecurityPageName.eventFilters,
title: i18n.translate('xpack.securitySolution.search.administration.eventFilters', {
defaultMessage: 'Event Filters',
}),
path: EVENT_FILTERS_PATH,
},
],
},
};
/**
* A function that generates the plugin deepLinks
* @param licenseType optional string for license level, if not provided basic is assumed.
*/
export function getDeepLinks(
licenseType?: LicenseType,
capabilities?: ApplicationStart['capabilities']
): AppDeepLink[] {
return topDeepLinks
.filter(
(deepLink) =>
deepLink.id !== SecurityPageName.case ||
capabilities == null ||
(deepLink.id === SecurityPageName.case && capabilities.siem.read_cases === true)
)
.map((deepLink) => {
const deepLinkId = deepLink.id as SecurityDeepLinkName;
const subPluginDeepLinks = nestedDeepLinks[deepLinkId];
const baseDeepLinks = Array.isArray(subPluginDeepLinks.base)
? [...subPluginDeepLinks.base]
: [];
if (
deepLinkId === SecurityPageName.case &&
capabilities != null &&
capabilities.siem.crud_cases === false
) {
return {
...deepLink,
deepLinks: [],
};
}
if (isPremiumLicense(licenseType) && subPluginDeepLinks?.premium) {
return {
...deepLink,
deepLinks: [...baseDeepLinks, ...subPluginDeepLinks.premium],
};
}
return {
...deepLink,
deepLinks: baseDeepLinks,
};
});
}
export function isPremiumLicense(licenseType?: LicenseType): boolean {
return (
licenseType === 'gold' ||
licenseType === 'platinum' ||
licenseType === 'enterprise' ||
licenseType === 'trial'
);
}
export function updateGlobalNavigation({
capabilities,
updater$,
}: {
capabilities: ApplicationStart['capabilities'];
updater$: Subject<AppUpdater>;
}) {
const deepLinks = getDeepLinks(undefined, capabilities);
const updatedDeepLinks = deepLinks.map((link) => {
switch (link.id) {
case 'case':
return {
...link,
navLinkStatus: capabilities.siem.read_cases
? AppNavLinkStatus.visible
: AppNavLinkStatus.hidden,
};
default:
return link;
}
});
updater$.next(() => ({
deepLinks: updatedDeepLinks,
}));
}

View file

@ -11,6 +11,7 @@ import {
EuiHeaderSectionItem,
} from '@elastic/eui';
import React, { useEffect, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { createPortalNode, OutPortal, InPortal } from 'react-reverse-portal';
import { i18n } from '@kbn/i18n';
@ -18,7 +19,8 @@ import { AppMountParameters } from '../../../../../../../src/core/public';
import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public';
import { MlPopover } from '../../../common/components/ml_popover/ml_popover';
import { useKibana } from '../../../common/lib/kibana';
import { ADD_DATA_PATH, APP_DETECTIONS_PATH } from '../../../../common/constants';
import { ADD_DATA_PATH } from '../../../../common/constants';
import { isDetectionsPath } from '../../../../public/helpers';
const BUTTON_ADD_DATA = i18n.translate('xpack.securitySolution.globalHeader.buttonAddData', {
defaultMessage: 'Add data',
@ -31,27 +33,29 @@ const BUTTON_ADD_DATA = i18n.translate('xpack.securitySolution.globalHeader.butt
export const GlobalHeader = React.memo(
({ setHeaderActionMenu }: { setHeaderActionMenu: AppMountParameters['setHeaderActionMenu'] }) => {
const portalNode = useMemo(() => createPortalNode(), []);
const { http } = useKibana().services;
const {
http: {
basePath: { prepend },
},
} = useKibana().services;
const { pathname } = useLocation();
useEffect(() => {
let unmount = () => {};
setHeaderActionMenu((element) => {
const mount = toMountPoint(<OutPortal node={portalNode} />);
unmount = mount(element);
return unmount;
return mount(element);
});
return () => {
portalNode.unmount();
unmount();
setHeaderActionMenu(undefined);
};
}, [portalNode, setHeaderActionMenu]);
return (
<InPortal node={portalNode}>
<EuiHeaderSection side="right">
{window.location.pathname.includes(APP_DETECTIONS_PATH) && (
{isDetectionsPath(pathname) && (
<EuiHeaderSectionItem>
<MlPopover />
</EuiHeaderSectionItem>
@ -61,7 +65,7 @@ export const GlobalHeader = React.memo(
<EuiHeaderLink
color="primary"
data-test-subj="add-data"
href={http.basePath.prepend(ADD_DATA_PATH)}
href={prepend(ADD_DATA_PATH)}
iconType="indexOpen"
>
{BUTTON_ADD_DATA}

View file

@ -0,0 +1,130 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as i18n from '../translations';
import { SecurityPageName, SecurityPageGroupName } from '../types';
import { SiemNavTab, NavTabGroups } from '../../common/components/navigation/types';
import {
APP_OVERVIEW_PATH,
APP_RULES_PATH,
APP_ALERTS_PATH,
APP_EXCEPTIONS_PATH,
APP_HOSTS_PATH,
APP_NETWORK_PATH,
APP_TIMELINES_PATH,
APP_CASES_PATH,
APP_MANAGEMENT_PATH,
APP_ENDPOINTS_PATH,
APP_TRUSTED_APPS_PATH,
APP_EVENT_FILTERS_PATH,
} from '../../../common/constants';
export const navTabs: SiemNavTab = {
[SecurityPageName.overview]: {
id: SecurityPageName.overview,
name: i18n.OVERVIEW,
href: APP_OVERVIEW_PATH,
disabled: false,
urlKey: 'overview',
},
[SecurityPageName.alerts]: {
id: SecurityPageName.alerts,
name: i18n.ALERTS,
href: APP_ALERTS_PATH,
disabled: false,
urlKey: SecurityPageName.alerts,
},
[SecurityPageName.rules]: {
id: SecurityPageName.rules,
name: i18n.RULES,
href: APP_RULES_PATH,
disabled: false,
urlKey: SecurityPageName.rules,
},
[SecurityPageName.exceptions]: {
id: SecurityPageName.exceptions,
name: i18n.EXCEPTIONS,
href: APP_EXCEPTIONS_PATH,
disabled: false,
urlKey: SecurityPageName.exceptions,
},
[SecurityPageName.hosts]: {
id: SecurityPageName.hosts,
name: i18n.HOSTS,
href: APP_HOSTS_PATH,
disabled: false,
urlKey: 'host',
},
[SecurityPageName.network]: {
id: SecurityPageName.network,
name: i18n.NETWORK,
href: APP_NETWORK_PATH,
disabled: false,
urlKey: 'network',
},
[SecurityPageName.timelines]: {
id: SecurityPageName.timelines,
name: i18n.TIMELINES,
href: APP_TIMELINES_PATH,
disabled: false,
urlKey: 'timeline',
},
[SecurityPageName.case]: {
id: SecurityPageName.case,
name: i18n.CASE,
href: APP_CASES_PATH,
disabled: false,
urlKey: 'case',
},
[SecurityPageName.administration]: {
id: SecurityPageName.administration,
name: i18n.ADMINISTRATION,
href: APP_MANAGEMENT_PATH,
disabled: false,
urlKey: SecurityPageName.administration,
},
[SecurityPageName.endpoints]: {
id: SecurityPageName.endpoints,
name: i18n.ENDPOINTS,
href: APP_ENDPOINTS_PATH,
disabled: false,
urlKey: SecurityPageName.administration,
},
[SecurityPageName.trustedApps]: {
id: SecurityPageName.trustedApps,
name: i18n.TRUSTED_APPLICATIONS,
href: APP_TRUSTED_APPS_PATH,
disabled: false,
urlKey: SecurityPageName.administration,
},
[SecurityPageName.eventFilters]: {
id: SecurityPageName.eventFilters,
name: i18n.EVENT_FILTERS,
href: APP_EVENT_FILTERS_PATH,
disabled: false,
urlKey: SecurityPageName.administration,
},
};
export const navTabGroups: NavTabGroups = {
[SecurityPageGroupName.detect]: {
id: SecurityPageGroupName.detect,
name: i18n.DETECT,
},
[SecurityPageGroupName.explore]: {
id: SecurityPageGroupName.explore,
name: i18n.EXPLORE,
},
[SecurityPageGroupName.investigate]: {
id: SecurityPageGroupName.investigate,
name: i18n.INVESTIGATE,
},
[SecurityPageGroupName.manage]: {
id: SecurityPageGroupName.manage,
name: i18n.MANAGE,
},
};

View file

@ -1,72 +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 * as i18n from '../translations';
import { SecurityPageName } from '../types';
import { SiemNavTab } from '../../common/components/navigation/types';
import {
APP_OVERVIEW_PATH,
APP_DETECTIONS_PATH,
APP_HOSTS_PATH,
APP_NETWORK_PATH,
APP_TIMELINES_PATH,
APP_CASES_PATH,
APP_MANAGEMENT_PATH,
} from '../../../common/constants';
export const navTabs: SiemNavTab = {
[SecurityPageName.overview]: {
id: SecurityPageName.overview,
name: i18n.OVERVIEW,
href: APP_OVERVIEW_PATH,
disabled: false,
urlKey: 'overview',
},
[SecurityPageName.detections]: {
id: SecurityPageName.detections,
name: i18n.DETECTION_ENGINE,
href: APP_DETECTIONS_PATH,
disabled: false,
urlKey: 'detections',
},
[SecurityPageName.hosts]: {
id: SecurityPageName.hosts,
name: i18n.HOSTS,
href: APP_HOSTS_PATH,
disabled: false,
urlKey: 'host',
},
[SecurityPageName.network]: {
id: SecurityPageName.network,
name: i18n.NETWORK,
href: APP_NETWORK_PATH,
disabled: false,
urlKey: 'network',
},
[SecurityPageName.timelines]: {
id: SecurityPageName.timelines,
name: i18n.TIMELINES,
href: APP_TIMELINES_PATH,
disabled: false,
urlKey: 'timeline',
},
[SecurityPageName.case]: {
id: SecurityPageName.case,
name: i18n.CASE,
href: APP_CASES_PATH,
disabled: false,
urlKey: 'case',
},
[SecurityPageName.administration]: {
id: SecurityPageName.administration,
name: i18n.ADMINISTRATION,
href: APP_MANAGEMENT_PATH,
disabled: false,
urlKey: SecurityPageName.administration,
},
};

View file

@ -5,7 +5,8 @@
* 2.0.
*/
import React, { useRef } from 'react';
import React from 'react';
import { useLocation } from 'react-router-dom';
import { DragDropContextWrapper } from '../../common/components/drag_and_drop/drag_drop_context_wrapper';
import { AppLeaveHandler, AppMountParameters } from '../../../../../../src/core/public';
@ -14,12 +15,11 @@ import { HelpMenu } from '../../common/components/help_menu';
import { UseUrlState } from '../../common/components/url_state';
import { navTabs } from './home_navigations';
import { useInitSourcerer, useSourcererScope } from '../../common/containers/sourcerer';
import { useKibana } from '../../common/lib/kibana';
import { DETECTIONS_SUB_PLUGIN_ID } from '../../../common/constants';
import { SourcererScopeName } from '../../common/store/sourcerer/model';
import { useUpgradeSecurityPackages } from '../../common/hooks/use_upgrade_security_packages';
import { GlobalHeader } from './global_header';
import { SecuritySolutionTemplateWrapper } from './template_wrapper';
import { isDetectionsPath } from '../../helpers';
interface HomePageProps {
children: React.ReactNode;
@ -32,23 +32,14 @@ const HomePageComponent: React.FC<HomePageProps> = ({
onAppLeave,
setHeaderActionMenu,
}) => {
const { application } = useKibana().services;
const subPluginId = useRef<string>('');
application.currentAppId$.subscribe((appId) => {
subPluginId.current = appId ?? '';
});
const { pathname } = useLocation();
useInitSourcerer(
subPluginId.current === DETECTIONS_SUB_PLUGIN_ID
? SourcererScopeName.detections
: SourcererScopeName.default
isDetectionsPath(pathname) ? SourcererScopeName.detections : SourcererScopeName.default
);
const { browserFields, indexPattern } = useSourcererScope(
subPluginId.current === DETECTIONS_SUB_PLUGIN_ID
? SourcererScopeName.detections
: SourcererScopeName.default
isDetectionsPath(pathname) ? SourcererScopeName.detections : SourcererScopeName.default
);
// side effect: this will attempt to upgrade the endpoint package if it is not up to date

View file

@ -7,34 +7,28 @@
/* eslint-disable react/display-name */
import React, { useRef } from 'react';
import React from 'react';
import { useLocation } from 'react-router-dom';
import { KibanaPageTemplateProps } from '../../../../../../../../src/plugins/kibana_react/public';
import { AppLeaveHandler } from '../../../../../../../../src/core/public';
import { useKibana } from '../../../../common/lib/kibana';
import { useShowTimeline } from '../../../../common/utils/timeline/use_show_timeline';
import { useSourcererScope } from '../../../../common/containers/sourcerer';
import { DETECTIONS_SUB_PLUGIN_ID } from '../../../../../common/constants';
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
import { TimelineId } from '../../../../../common/types/timeline';
import { AutoSaveWarningMsg } from '../../../../timelines/components/timeline/auto_save_warning';
import { Flyout } from '../../../../timelines/components/flyout';
import { isDetectionsPath } from '../../../../../public/helpers';
export const BOTTOM_BAR_CLASSNAME = 'timeline-bottom-bar';
export const SecuritySolutionBottomBar = React.memo(
({ onAppLeave }: { onAppLeave: (handler: AppLeaveHandler) => void }) => {
const subPluginId = useRef<string>('');
const { application } = useKibana().services;
application.currentAppId$.subscribe((appId) => {
subPluginId.current = appId ?? '';
});
const { pathname } = useLocation();
const [showTimeline] = useShowTimeline();
const { indicesExist } = useSourcererScope(
subPluginId.current === DETECTIONS_SUB_PLUGIN_ID
? SourcererScopeName.detections
: SourcererScopeName.default
isDetectionsPath(pathname) ? SourcererScopeName.detections : SourcererScopeName.default
);
return indicesExist && showTimeline ? (

View file

@ -7,7 +7,10 @@
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { Redirect, Route, Switch } from 'react-router-dom';
import { OVERVIEW_PATH } from '../../common/constants';
import { NotFoundPage } from '../app/404';
import { SecurityApp } from './app';
import { RenderAppProps } from './types';
@ -18,8 +21,11 @@ export const renderApp = ({
setHeaderActionMenu,
services,
store,
SubPluginRoutes,
usageCollection,
subPlugins,
}: RenderAppProps): (() => void) => {
const ApplicationUsageTrackingProvider =
usageCollection?.components.ApplicationUsageTrackingProvider ?? React.Fragment;
render(
<SecurityApp
history={history}
@ -28,7 +34,30 @@ export const renderApp = ({
setHeaderActionMenu={setHeaderActionMenu}
store={store}
>
<SubPluginRoutes />
<ApplicationUsageTrackingProvider>
<Switch>
{[
...subPlugins.overview.routes,
...subPlugins.alerts.routes,
...subPlugins.rules.routes,
...subPlugins.exceptions.routes,
...subPlugins.hosts.routes,
...subPlugins.network.routes,
...subPlugins.timelines.routes,
...subPlugins.cases.routes,
...subPlugins.management.routes,
].map((route, index) => (
<Route key={`route-${index}`} {...route} />
))}
<Route path="" exact>
<Redirect to={OVERVIEW_PATH} />
</Route>
<Route>
<NotFoundPage />
</Route>
</Switch>
</ApplicationUsageTrackingProvider>
</SecurityApp>,
element
);

View file

@ -1,22 +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 { getDeepLinksAndKeywords } from '.';
import { SecurityPageName } from '../../../common/constants';
describe('public search functions', () => {
it('returns a subset of links for basic license, full set for platinum', () => {
const basicLicense = 'basic';
const platinumLicense = 'platinum';
for (const pageName of Object.values(SecurityPageName)) {
expect.assertions(Object.values(SecurityPageName).length * 2);
const basicLinkCount = getDeepLinksAndKeywords(pageName, basicLicense).deepLinks?.length || 0;
const platinumLinks = getDeepLinksAndKeywords(pageName, platinumLicense);
expect(platinumLinks.deepLinks?.length).toBeGreaterThanOrEqual(basicLinkCount);
expect(platinumLinks.keywords?.length).not.toBe(null);
}
});
});

View file

@ -1,240 +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 { Subject } from 'rxjs';
import { AppUpdater } from 'src/core/public';
import { LicenseType } from '../../../../licensing/common/types';
import { SecuritySubPluginNames, SecurityDeepLinks } from '../types';
import { App } from '../../../../../../src/core/public';
const securityDeepLinks: SecurityDeepLinks = {
detections: {
base: [
{
id: 'siemDetectionRules',
title: i18n.translate('xpack.securitySolution.search.detections.manage', {
defaultMessage: 'Manage Rules',
}),
keywords: ['rules'],
path: '/rules',
},
],
},
hosts: {
base: [
{
id: 'authentications',
title: i18n.translate('xpack.securitySolution.search.hosts.authentications', {
defaultMessage: 'Authentications',
}),
path: '/authentications',
},
{
id: 'uncommonProcesses',
title: i18n.translate('xpack.securitySolution.search.hosts.uncommonProcesses', {
defaultMessage: 'Uncommon Processes',
}),
path: '/uncommonProcesses',
},
{
id: 'events',
title: i18n.translate('xpack.securitySolution.search.hosts.events', {
defaultMessage: 'Events',
}),
path: '/events',
},
{
id: 'externalAlerts',
title: i18n.translate('xpack.securitySolution.search.hosts.externalAlerts', {
defaultMessage: 'External Alerts',
}),
path: '/alerts',
},
],
premium: [
{
id: 'anomalies',
title: i18n.translate('xpack.securitySolution.search.hosts.anomalies', {
defaultMessage: 'Anomalies',
}),
path: '/anomalies',
},
],
},
network: {
base: [
{
id: 'dns',
title: i18n.translate('xpack.securitySolution.search.network.dns', {
defaultMessage: 'DNS',
}),
path: '/dns',
},
{
id: 'http',
title: i18n.translate('xpack.securitySolution.search.network.http', {
defaultMessage: 'HTTP',
}),
path: '/http',
},
{
id: 'tls',
title: i18n.translate('xpack.securitySolution.search.network.tls', {
defaultMessage: 'TLS',
}),
path: '/tls',
},
{
id: 'externalAlertsNetwork',
title: i18n.translate('xpack.securitySolution.search.network.externalAlerts', {
defaultMessage: 'External Alerts',
}),
path: '/external-alerts',
},
],
premium: [
{
id: 'anomalies',
title: i18n.translate('xpack.securitySolution.search.hosts.anomalies', {
defaultMessage: 'Anomalies',
}),
path: '/anomalies',
},
],
},
timelines: {
base: [
{
id: 'timelineTemplates',
title: i18n.translate('xpack.securitySolution.search.timeline.templates', {
defaultMessage: 'Templates',
}),
path: '/template',
},
],
},
overview: {
base: [],
},
case: {
base: [
{
id: 'create',
title: i18n.translate('xpack.securitySolution.search.cases.create', {
defaultMessage: 'Create New Case',
}),
path: '/create',
},
],
premium: [
{
id: 'configure',
title: i18n.translate('xpack.securitySolution.search.cases.configure', {
defaultMessage: 'Configure Cases',
}),
path: '/configure',
},
],
},
administration: {
base: [
{
id: 'trustApplications',
title: i18n.translate('xpack.securitySolution.search.administration.trustedApps', {
defaultMessage: 'Trusted Applications',
}),
path: '/trusted_apps',
},
],
},
};
const subpluginKeywords: { [key in SecuritySubPluginNames]: string[] } = {
detections: [
i18n.translate('xpack.securitySolution.search.detections', {
defaultMessage: 'Detections',
}),
],
hosts: [
i18n.translate('xpack.securitySolution.search.hosts', {
defaultMessage: 'Hosts',
}),
],
network: [
i18n.translate('xpack.securitySolution.search.network', {
defaultMessage: 'Network',
}),
],
timelines: [
i18n.translate('xpack.securitySolution.search.timelines', {
defaultMessage: 'Timelines',
}),
],
overview: [
i18n.translate('xpack.securitySolution.search.overview', {
defaultMessage: 'Overview',
}),
],
case: [
i18n.translate('xpack.securitySolution.search.cases', {
defaultMessage: 'Cases',
}),
],
administration: [
i18n.translate('xpack.securitySolution.search.administration', {
defaultMessage: 'Endpoint Administration',
}),
],
};
/**
* A function that generates a subPlugin's meta tag
* @param subPluginName SubPluginName of the app to retrieve meta information for.
* @param licenseType optional string for license level, if not provided basic is assumed.
*/
export function getDeepLinksAndKeywords(
subPluginName: SecuritySubPluginNames,
licenseType?: LicenseType
): Pick<App, 'deepLinks' | 'keywords'> {
const baseRoutes = [...securityDeepLinks[subPluginName].base];
if (
licenseType === 'gold' ||
licenseType === 'platinum' ||
licenseType === 'enterprise' ||
licenseType === 'trial'
) {
const premiumRoutes =
securityDeepLinks[subPluginName] && securityDeepLinks[subPluginName].premium;
if (premiumRoutes !== undefined) {
return {
keywords: subpluginKeywords[subPluginName],
deepLinks: [...baseRoutes, ...premiumRoutes],
};
}
}
return {
keywords: subpluginKeywords[subPluginName],
deepLinks: baseRoutes,
};
}
/**
* A function that updates a subplugin's meta property as appropriate when license level changes.
* @param subPluginName SubPluginName of the app to register deepLinks for
* @param appUpdater an instance of appUpdater$ observable to update search links when needed.
* @param licenseType A string representing the current license level.
*/
export function registerDeepLinks(
subPluginName: SecuritySubPluginNames,
appUpdater?: Subject<AppUpdater>,
licenseType?: LicenseType
) {
if (appUpdater !== undefined) {
appUpdater.next(() => ({ ...getDeepLinksAndKeywords(subPluginName, licenseType) }));
}
}

View file

@ -19,12 +19,13 @@ export const NETWORK = i18n.translate('xpack.securitySolution.navigation.network
defaultMessage: 'Network',
});
export const DETECTION_ENGINE = i18n.translate(
'xpack.securitySolution.navigation.detectionEngine',
{
defaultMessage: 'Detections',
}
);
export const RULES = i18n.translate('xpack.securitySolution.navigation.rules', {
defaultMessage: 'Rules',
});
export const EXCEPTIONS = i18n.translate('xpack.securitySolution.navigation.exceptions', {
defaultMessage: 'Exceptions',
});
export const ALERTS = i18n.translate('xpack.securitySolution.navigation.alerts', {
defaultMessage: 'Alerts',
@ -41,3 +42,31 @@ export const CASE = i18n.translate('xpack.securitySolution.navigation.case', {
export const ADMINISTRATION = i18n.translate('xpack.securitySolution.navigation.administration', {
defaultMessage: 'Administration',
});
export const ENDPOINTS = i18n.translate('xpack.securitySolution.search.administration.endpoints', {
defaultMessage: 'Endpoints',
});
export const TRUSTED_APPLICATIONS = i18n.translate(
'xpack.securitySolution.search.administration.trustedApps',
{
defaultMessage: 'Trusted Applications',
}
);
export const EVENT_FILTERS = i18n.translate(
'xpack.securitySolution.search.administration.eventFilters',
{
defaultMessage: 'Event Filters',
}
);
export const DETECT = i18n.translate('xpack.securitySolution.navigation.detect', {
defaultMessage: 'Detect',
});
export const EXPLORE = i18n.translate('xpack.securitySolution.navigation.explore', {
defaultMessage: 'Explore',
});
export const INVESTIGATE = i18n.translate('xpack.securitySolution.navigation.investigate', {
defaultMessage: 'Investigate',
});
export const MANAGE = i18n.translate('xpack.securitySolution.navigation.manage', {
defaultMessage: 'Manage',
});

View file

@ -16,9 +16,10 @@ import {
StateFromReducersMapObject,
CombinedState,
} from 'redux';
import { RouteProps } from 'react-router-dom';
import { AppMountParameters, AppDeepLink } from '../../../../../src/core/public';
import { StartServices } from '../types';
import { UsageCollectionSetup } from '../../../../../src/plugins/usage_collection/public';
import { StartedSubPlugins, StartServices } from '../types';
/**
* The React properties used to render `SecurityApp` as well as the `element` to render it into.
@ -26,7 +27,8 @@ import { StartServices } from '../types';
export interface RenderAppProps extends AppMountParameters {
services: StartServices;
store: Store<State, Action>;
SubPluginRoutes: React.FC;
subPlugins: StartedSubPlugins;
usageCollection?: UsageCollectionSetup;
}
import { State, SubPluginsInitReducer } from '../common/store';
@ -34,7 +36,7 @@ import { Immutable } from '../../common/endpoint/types';
import { AppAction } from '../common/store/actions';
import { TimelineState } from '../timelines/store/timeline/types';
import { SecurityPageName } from '../../common/constants';
export { SecurityPageName } from '../../common/constants';
export { SecurityPageName, SecurityPageGroupName } from '../../common/constants';
export interface SecuritySubPluginStore<K extends SecuritySubPluginKeyStore, T> {
initialState: Record<K, T | undefined>;
@ -42,8 +44,10 @@ export interface SecuritySubPluginStore<K extends SecuritySubPluginKeyStore, T>
middleware?: Array<Middleware<{}, State, Dispatch<AppAction | Immutable<AppAction>>>>;
}
export type SecuritySubPluginRoutes = RouteProps[];
export interface SecuritySubPlugin {
SubPluginRoutes: React.FC;
routes: SecuritySubPluginRoutes;
storageTimelines?: Pick<TimelineState, 'timelineById'>;
}
@ -55,14 +59,21 @@ export type SecuritySubPluginKeyStore =
| 'alertList'
| 'management';
export type SecuritySubPluginNames = keyof typeof SecurityPageName;
export type SecurityDeepLinkName =
| SecurityPageName.overview
| SecurityPageName.detections
| SecurityPageName.hosts
| SecurityPageName.network
| SecurityPageName.timelines
| SecurityPageName.case
| SecurityPageName.administration;
interface SecurityDeepLink {
base: AppDeepLink[];
premium?: AppDeepLink[];
}
export type SecurityDeepLinks = { [key in SecuritySubPluginNames]: SecurityDeepLink };
export type SecurityDeepLinks = { [key in SecurityDeepLinkName]: SecurityDeepLink };
/**
* Returned by the various 'SecuritySubPlugin' classes from the `start` method.

View file

@ -15,7 +15,7 @@ import {
} from '../../../common/components/link_to';
import { SecurityPageName } from '../../../app/types';
import { useKibana } from '../../../common/lib/kibana';
import { APP_ID, CASES_APP_ID } from '../../../../common/constants';
import { APP_ID } from '../../../../common/constants';
export interface AllCasesNavProps {
detailName: string;
@ -36,7 +36,8 @@ export const AllCases = React.memo<AllCasesProps>(({ userCanCrud }) => {
const goToCreateCase = useCallback(
async (ev) => {
ev.preventDefault();
return navigateToApp(CASES_APP_ID, {
return navigateToApp(APP_ID, {
deepLinkId: SecurityPageName.case,
path: getCreateCaseUrl(urlSearch),
});
},
@ -46,7 +47,8 @@ export const AllCases = React.memo<AllCasesProps>(({ userCanCrud }) => {
const goToCaseConfigure = useCallback(
async (ev) => {
ev.preventDefault();
return navigateToApp(CASES_APP_ID, {
return navigateToApp(APP_ID, {
deepLinkId: SecurityPageName.case,
path: getConfigureCasesUrl(urlSearch),
});
},
@ -59,7 +61,8 @@ export const AllCases = React.memo<AllCasesProps>(({ userCanCrud }) => {
return formatUrl(getCaseDetailsUrl({ id: detailName, subCaseId }));
},
onClick: async ({ detailName, subCaseId, search }: AllCasesNavProps) => {
return navigateToApp(CASES_APP_ID, {
return navigateToApp(APP_ID, {
deepLinkId: SecurityPageName.case,
path: getCaseDetailsUrl({ id: detailName, search, subCaseId }),
});
},

View file

@ -23,11 +23,7 @@ import { Case, CaseViewRefreshPropInterface } from '../../../../../cases/common'
import { TimelineId } from '../../../../common/types/timeline';
import { SecurityPageName } from '../../../app/types';
import { KibanaServices, useKibana } from '../../../common/lib/kibana';
import {
APP_ID,
CASES_APP_ID,
DETECTION_ENGINE_QUERY_SIGNALS_URL,
} from '../../../../common/constants';
import { APP_ID, DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../common/constants';
import { timelineActions } from '../../../timelines/store/timeline';
import { useSourcererScope } from '../../../common/containers/sourcerer';
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
@ -132,7 +128,7 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) =
const dispatch = useDispatch();
const { formatUrl, search } = useFormatUrl(SecurityPageName.case);
const { formatUrl: detectionsFormatUrl, search: detectionsUrlSearch } = useFormatUrl(
SecurityPageName.detections
SecurityPageName.rules
);
const allCasesLink = getCaseUrl(search);
@ -190,7 +186,8 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) =
if (e) {
e.preventDefault();
}
return navigateToApp(CASES_APP_ID, {
return navigateToApp(APP_ID, {
deepLinkId: SecurityPageName.case,
path: allCasesLink,
});
},
@ -201,7 +198,8 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) =
if (e) {
e.preventDefault();
}
return navigateToApp(CASES_APP_ID, {
return navigateToApp(APP_ID, {
deepLinkId: SecurityPageName.case,
path: getCaseDetailsUrl({ id: caseId }),
});
},
@ -213,7 +211,8 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) =
if (e) {
e.preventDefault();
}
return navigateToApp(CASES_APP_ID, {
return navigateToApp(APP_ID, {
deepLinkId: SecurityPageName.case,
path: getConfigureCasesUrl(search),
});
},
@ -227,7 +226,8 @@ export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) =
if (e) {
e.preventDefault();
}
return navigateToApp(`${APP_ID}:${SecurityPageName.detections}`, {
return navigateToApp(APP_ID, {
deepLinkId: SecurityPageName.rules,
path: getRuleDetailsUrl(ruleId ?? ''),
});
},

View file

@ -16,7 +16,7 @@ import { Create } from '.';
import { useKibana } from '../../../common/lib/kibana';
import { Case } from '../../../../../cases/public/containers/types';
import { basicCase } from '../../../../../cases/public/containers/mock';
import { APP_ID, CASES_APP_ID } from '../../../../common/constants';
import { APP_ID, SecurityPageName } from '../../../../common/constants';
import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search';
jest.mock('../use_insert_timeline');
@ -71,8 +71,9 @@ describe('Create case', () => {
);
await waitFor(() =>
expect(mockNavigateToApp).toHaveBeenCalledWith(CASES_APP_ID, {
expect(mockNavigateToApp).toHaveBeenCalledWith(APP_ID, {
path: `?${mockRes}`,
deepLinkId: SecurityPageName.case,
})
);
});
@ -95,8 +96,9 @@ describe('Create case', () => {
);
await waitFor(() =>
expect(mockNavigateToApp).toHaveBeenNthCalledWith(1, CASES_APP_ID, {
expect(mockNavigateToApp).toHaveBeenNthCalledWith(1, APP_ID, {
path: `/basic-case-id?${mockRes}`,
deepLinkId: SecurityPageName.case,
})
);
});

View file

@ -12,9 +12,10 @@ import { getCaseDetailsUrl, getCaseUrl } from '../../../common/components/link_t
import { useKibana } from '../../../common/lib/kibana';
import * as timelineMarkdownPlugin from '../../../common/components/markdown_editor/plugins/timeline';
import { useInsertTimeline } from '../use_insert_timeline';
import { APP_ID, CASES_APP_ID } from '../../../../common/constants';
import { APP_ID } from '../../../../common/constants';
import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search';
import { navTabs } from '../../../app/home/home_navigations';
import { SecurityPageName } from '../../../app/types';
export const Create = React.memo(() => {
const {
@ -24,14 +25,16 @@ export const Create = React.memo(() => {
const search = useGetUrlSearch(navTabs.case);
const onSuccess = useCallback(
async ({ id }) =>
navigateToApp(CASES_APP_ID, {
navigateToApp(APP_ID, {
deepLinkId: SecurityPageName.case,
path: getCaseDetailsUrl({ id, search }),
}),
[navigateToApp, search]
);
const handleSetIsCancel = useCallback(
async () =>
navigateToApp(CASES_APP_ID, {
navigateToApp(APP_ID, {
deepLinkId: SecurityPageName.case,
path: getCaseUrl(search),
}),
[navigateToApp, search]

View file

@ -15,6 +15,7 @@ import { TestProviders } from '../../../common/mock';
import { AddToCaseAction } from './add_to_case_action';
import { basicCase } from '../../../../../cases/public/containers/mock';
import { Case, SECURITY_SOLUTION_OWNER } from '../../../../../cases/common';
import { APP_ID, SecurityPageName } from '../../../../common/constants';
jest.mock('../../../common/lib/kibana');
jest.mock('../../../common/components/link_to', () => {
@ -177,8 +178,9 @@ describe('AddToCaseAction', () => {
.first()
.simulate('click');
expect(mockNavigateToApp).toHaveBeenCalledWith('securitySolution:case', {
expect(mockNavigateToApp).toHaveBeenCalledWith(APP_ID, {
path: '/basic-case-id',
deepLinkId: SecurityPageName.case,
});
});

View file

@ -17,7 +17,7 @@ import {
} from '@elastic/eui';
import { Case, CaseStatuses, StatusAll } from '../../../../../cases/common';
import { APP_ID, CASES_APP_ID } from '../../../../common/constants';
import { APP_ID } from '../../../../common/constants';
import { Ecs } from '../../../../common/ecs';
import { SecurityPageName } from '../../../app/types';
import {
@ -80,7 +80,8 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
const onViewCaseClick = useCallback(
(id) => {
navigateToApp(CASES_APP_ID, {
navigateToApp(APP_ID, {
deepLinkId: SecurityPageName.case,
path: getCaseDetailsUrl({ id }),
});
},
@ -134,7 +135,8 @@ const AddToCaseActionComponent: React.FC<AddToCaseActionProps> = ({
const goToCreateCase = useCallback(
async (ev) => {
ev.preventDefault();
return navigateToApp(CASES_APP_ID, {
return navigateToApp(APP_ID, {
deepLinkId: SecurityPageName.case,
path: getCreateCaseUrl(urlSearch),
});
},

View file

@ -6,14 +6,14 @@
*/
import { SecuritySubPlugin } from '../app/types';
import { CasesRoutes } from './routes';
import { routes } from './routes';
export class Cases {
public setup() {}
public start(): SecuritySubPlugin {
return {
SubPluginRoutes: CasesRoutes,
routes,
};
}
}

View file

@ -16,7 +16,7 @@ import { useGetUserCasesPermissions, useKibana } from '../../common/lib/kibana';
import { getCaseUrl } from '../../common/components/link_to';
import { navTabs } from '../../app/home/home_navigations';
import { CaseView } from '../components/case_view';
import { CASES_APP_ID } from '../../../common/constants';
import { APP_ID } from '../../../common/constants';
export const CaseDetailsPage = React.memo(() => {
const {
@ -31,9 +31,12 @@ export const CaseDetailsPage = React.memo(() => {
useEffect(() => {
if (userPermissions != null && !userPermissions.read) {
navigateToApp(CASES_APP_ID, { path: getCaseUrl(search) });
navigateToApp(APP_ID, {
deepLinkId: SecurityPageName.case,
path: getCaseUrl(search),
});
}
}, [navigateToApp, userPermissions, search]);
}, [userPermissions, navigateToApp, search]);
return caseId != null ? (
<>

View file

@ -18,7 +18,8 @@ import { navTabs } from '../../app/home/home_navigations';
import { CaseHeaderPage } from '../components/case_header_page';
import { WhitePageWrapper, SectionWrapper } from '../components/wrappers';
import * as i18n from './translations';
import { APP_ID, CASES_APP_ID } from '../../../common/constants';
import { APP_ID } from '../../../common/constants';
import { SiemNavTabKey } from '../../common/components/navigation/types';
const ConfigureCasesPageComponent: React.FC = () => {
const {
@ -32,14 +33,15 @@ const ConfigureCasesPageComponent: React.FC = () => {
() => ({
href: getCaseUrl(search),
text: i18n.BACK_TO_ALL,
pageId: SecurityPageName.case,
pageId: SecurityPageName.case as SiemNavTabKey,
}),
[search]
);
useEffect(() => {
if (userPermissions != null && !userPermissions.read) {
navigateToApp(CASES_APP_ID, {
navigateToApp(APP_ID, {
deepLinkId: SecurityPageName.case,
path: getCaseUrl(search),
});
}

View file

@ -17,7 +17,8 @@ import { navTabs } from '../../app/home/home_navigations';
import { CaseHeaderPage } from '../components/case_header_page';
import { Create } from '../components/create';
import * as i18n from './translations';
import { CASES_APP_ID } from '../../../common/constants';
import { APP_ID } from '../../../common/constants';
import { SiemNavTabKey } from '../../common/components/navigation/types';
export const CreateCasePage = React.memo(() => {
const userPermissions = useGetUserCasesPermissions();
@ -30,14 +31,15 @@ export const CreateCasePage = React.memo(() => {
() => ({
href: getCaseUrl(search),
text: i18n.BACK_TO_ALL,
pageId: SecurityPageName.case,
pageId: SecurityPageName.case as SiemNavTabKey,
}),
[search]
);
useEffect(() => {
if (userPermissions != null && !userPermissions.crud) {
navigateToApp(CASES_APP_ID, {
navigateToApp(APP_ID, {
deepLinkId: SecurityPageName.case,
path: getCaseUrl(search),
});
}

View file

@ -14,14 +14,14 @@ import { CasesPage } from './case';
import { CreateCasePage } from './create_case';
import { ConfigureCasesPage } from './configure_cases';
import { useGetUserCasesPermissions, useKibana } from '../../common/lib/kibana';
import { CASES_PATH } from '../../../common/constants';
const casesPagePath = '';
const caseDetailsPagePath = `${casesPagePath}/:detailName`;
const caseDetailsPagePath = `${CASES_PATH}/:detailName`;
const subCaseDetailsPagePath = `${caseDetailsPagePath}/sub-cases/:subCaseId`;
const caseDetailsPagePathWithCommentId = `${caseDetailsPagePath}/:commentId`;
const subCaseDetailsPagePathWithCommentId = `${subCaseDetailsPagePath}/:commentId`;
const createCasePagePath = `${casesPagePath}/create`;
const configureCasesPagePath = `${casesPagePath}/configure`;
const createCasePagePath = `${CASES_PATH}/create`;
const configureCasesPagePath = `${CASES_PATH}/configure`;
const CaseContainerComponent: React.FC = () => {
const userPermissions = useGetUserCasesPermissions();
@ -63,7 +63,7 @@ const CaseContainerComponent: React.FC = () => {
<Route path={caseDetailsPagePath}>
<CaseDetailsPage />
</Route>
<Route strict exact path={casesPagePath}>
<Route strict exact path={CASES_PATH}>
<CasesPage />
</Route>
</Switch>

View file

@ -13,7 +13,8 @@ import { getCaseDetailsUrl, getCreateCaseUrl } from '../../common/components/lin
import { RouteSpyState } from '../../common/utils/route/types';
import * as i18n from './translations';
import { GetUrlForApp } from '../../common/components/navigation/types';
import { CASES_APP_ID } from '../../../common/constants';
import { APP_ID } from '../../../common/constants';
import { SecurityPageName } from '../../app/types';
export const getBreadcrumbs = (
params: RouteSpyState,
@ -25,7 +26,8 @@ export const getBreadcrumbs = (
let breadcrumb = [
{
text: i18n.PAGE_TITLE,
href: getUrlForApp(CASES_APP_ID, {
href: getUrlForApp(APP_ID, {
deepLinkId: SecurityPageName.case,
path: queryParameters,
}),
},
@ -35,7 +37,8 @@ export const getBreadcrumbs = (
...breadcrumb,
{
text: i18n.CREATE_BC_TITLE,
href: getUrlForApp(CASES_APP_ID, {
href: getUrlForApp(APP_ID, {
deepLinkId: SecurityPageName.case,
path: getCreateCaseUrl(queryParameters),
}),
},
@ -45,7 +48,8 @@ export const getBreadcrumbs = (
...breadcrumb,
{
text: params.state?.caseTitle ?? '',
href: getUrlForApp(CASES_APP_ID, {
href: getUrlForApp(APP_ID, {
deepLinkId: SecurityPageName.case,
path: getCaseDetailsUrl({ id: params.detailName, search: queryParameters }),
}),
},

View file

@ -6,16 +6,21 @@
*/
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import { TrackApplicationView } from '../../../../../src/plugins/usage_collection/public';
import { SecurityPageName, SecuritySubPluginRoutes } from '../app/types';
import { CASES_PATH } from '../../common/constants';
import { Case } from './pages';
import { NotFoundPage } from '../app/404';
export const CasesRoutes: React.FC = () => (
<Switch>
<Route path="/">
<Case />
</Route>
<Route render={() => <NotFoundPage />} />
</Switch>
export const CasesRoutes = () => (
<TrackApplicationView viewId={SecurityPageName.case}>
<Case />
</TrackApplicationView>
);
export const routes: SecuritySubPluginRoutes = [
{
path: CASES_PATH,
render: CasesRoutes,
},
];

View file

@ -8,10 +8,13 @@
import React, { memo, MouseEventHandler } from 'react';
import { EuiLink, EuiLinkProps, EuiButton, EuiButtonProps } from '@elastic/eui';
import { useNavigateToAppEventHandler } from '../../hooks/endpoint/use_navigate_to_app_event_handler';
import { APP_ID } from '../../../../common/constants';
export type LinkToAppProps = (EuiLinkProps | EuiButtonProps) & {
/** the app id - normally the value of the `id` in that plugin's `kibana.json` */
appId: string;
appId?: string;
/** optional app deep link id */
deepLinkId?: string;
/** Any app specific path (route) */
appPath?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -26,8 +29,17 @@ export type LinkToAppProps = (EuiLinkProps | EuiButtonProps) & {
* a given app without causing a full browser refresh
*/
export const LinkToApp = memo<LinkToAppProps>(
({ appId, appPath: path, appState: state, onClick, asButton, children, ...otherProps }) => {
const handleOnClick = useNavigateToAppEventHandler(appId, { path, state, onClick });
({
appId = APP_ID,
deepLinkId,
appPath: path,
appState: state,
onClick,
asButton,
children,
...otherProps
}) => {
const handleOnClick = useNavigateToAppEventHandler(appId, { deepLinkId, path, state, onClick });
return (
<>

View file

@ -9,7 +9,8 @@ import React, { memo, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { AppLocation } from '../../../../common/endpoint/types';
import { AppAction } from '../../store/actions';
import { timelineActions } from '../../../timelines/store/timeline';
import { TimelineId } from '../../../../../timelines/common';
/**
* This component should be used above all routes, but below the Provider.
@ -17,7 +18,11 @@ import { AppAction } from '../../store/actions';
*/
export const RouteCapture = memo(({ children }) => {
const location: AppLocation = useLocation();
const dispatch: (action: AppAction) => unknown = useDispatch();
const dispatch = useDispatch();
useEffect(() => {
dispatch(timelineActions.showTimeline({ id: TimelineId.active, show: false }));
}, [dispatch, location.pathname]);
useEffect(() => {
dispatch({ type: 'userChangedUrl', payload: location });

View file

@ -15,17 +15,7 @@ import { HeaderPage } from './index';
import { useMountAppended } from '../../utils/use_mount_appended';
import { SecurityPageName } from '../../../app/types';
jest.mock('react-router-dom', () => {
const original = jest.requireActual('react-router-dom');
return {
...original,
useHistory: () => ({
useHistory: jest.fn(),
}),
};
});
jest.mock('../../lib/kibana');
jest.mock('../link_to');
describe('HeaderPage', () => {

View file

@ -13,7 +13,6 @@ import {
EuiSpacer,
} from '@elastic/eui';
import React, { useCallback } from 'react';
import { useHistory } from 'react-router-dom';
import styled, { css } from 'styled-components';
import { LinkIcon, LinkIconProps } from '../link_icon';
@ -24,6 +23,8 @@ import { useFormatUrl } from '../link_to';
import { SecurityPageName } from '../../../app/types';
import { Sourcerer } from '../sourcerer';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { useKibana } from '../../lib/kibana';
import { SiemNavTabKey } from '../navigation/types';
interface HeaderProps {
border?: boolean;
@ -64,10 +65,10 @@ const HeaderSection = styled(EuiPageHeaderSection)`
HeaderSection.displayName = 'HeaderSection';
interface BackOptions {
href: LinkIconProps['href'];
text: LinkIconProps['children'];
href?: LinkIconProps['href'];
dataTestSubj?: string;
pageId: SecurityPageName;
pageId: SiemNavTabKey;
}
export interface HeaderPageProps extends HeaderProps {
@ -99,16 +100,18 @@ const HeaderPageComponent: React.FC<HeaderPageProps> = ({
titleNode,
...rest
}) => {
const history = useHistory();
const { navigateToUrl } = useKibana().services.application;
const { formatUrl } = useFormatUrl(backOptions?.pageId ?? SecurityPageName.overview);
const backUrl = formatUrl(backOptions?.href ?? '');
const goTo = useCallback(
(ev) => {
ev.preventDefault();
if (backOptions) {
history.push(backOptions.href ?? '');
navigateToUrl(backUrl);
}
},
[backOptions, history]
[backOptions, navigateToUrl, backUrl]
);
return (
<>
@ -119,7 +122,7 @@ const HeaderPageComponent: React.FC<HeaderPageProps> = ({
<LinkIcon
dataTestSubj={backOptions.dataTestSubj ?? 'link-back'}
onClick={goTo}
href={formatUrl(backOptions.href ?? '')}
href={backUrl}
iconType="arrowLeft"
>
{backOptions.text}
@ -128,7 +131,6 @@ const HeaderPageComponent: React.FC<HeaderPageProps> = ({
)}
{!backOptions && backComponent && <>{backComponent}</>}
{titleNode || (
<Title
draggableArguments={draggableArguments}

View file

@ -11,7 +11,7 @@ export { getDetectionEngineUrl } from '../redirect_to_detection_engine';
export { getAppOverviewUrl } from '../redirect_to_overview';
export { getHostDetailsUrl, getHostsUrl } from '../redirect_to_hosts';
export { getNetworkUrl, getNetworkDetailsUrl } from '../redirect_to_network';
export { getTimelinesUrl, getTimelineTabsUrl, getTimelineUrl } from '../redirect_to_timelines';
export { getTimelineTabsUrl, getTimelineUrl } from '../redirect_to_timelines';
export {
getCaseDetailsUrl,
getCaseUrl,

View file

@ -7,17 +7,17 @@
import { isEmpty } from 'lodash/fp';
import { useCallback } from 'react';
import { SecurityPageName } from '../../../app/types';
import { useGetUrlSearch } from '../navigation/use_get_url_search';
import { navTabs } from '../../../app/home/home_navigations';
import { APP_ID } from '../../../../common/constants';
import { useKibana } from '../../lib/kibana';
import { SiemNavTabKey } from '../navigation/types';
export { getDetectionEngineUrl, getRuleDetailsUrl } from './redirect_to_detection_engine';
export { getAppOverviewUrl } from './redirect_to_overview';
export { getHostDetailsUrl, getHostsUrl } from './redirect_to_hosts';
export { getNetworkUrl, getNetworkDetailsUrl } from './redirect_to_network';
export { getTimelinesUrl, getTimelineTabsUrl, getTimelineUrl } from './redirect_to_timelines';
export { getTimelineTabsUrl, getTimelineUrl } from './redirect_to_timelines';
export {
getCaseDetailsUrl,
getCaseUrl,
@ -33,7 +33,7 @@ interface FormatUrlOptions {
export type FormatUrl = (path: string, options?: Partial<FormatUrlOptions>) => string;
export const useFormatUrl = (page: SecurityPageName) => {
export const useFormatUrl = (page: SiemNavTabKey) => {
const { getUrlForApp } = useKibana().services.application;
const search = useGetUrlSearch(navTabs[page]);
const formatUrl = useCallback<FormatUrl>(
@ -48,12 +48,10 @@ export const useFormatUrl = (page: SecurityPageName) => {
? ''
: `?${pathArr[1]}`
}`;
return getUrlForApp(`${APP_ID}:${page}`, {
path: formattedPath,
absolute,
});
return getUrlForApp(APP_ID, { deepLinkId: page, path: formattedPath, absolute });
},
[getUrlForApp, page, search]
);
return { formatUrl, search };
};

View file

@ -9,15 +9,12 @@ import { appendSearch } from './helpers';
export const getDetectionEngineUrl = (search?: string) => `${appendSearch(search)}`;
export const getDetectionEngineTabUrl = (tabPath: string, search?: string) =>
`/${tabPath}${appendSearch(search)}`;
export const getRulesUrl = (search?: string) => `${appendSearch(search)}`;
export const getRulesUrl = (search?: string) => `/rules${appendSearch(search)}`;
export const getCreateRuleUrl = (search?: string) => `/rules/create${appendSearch(search)}`;
export const getCreateRuleUrl = (search?: string) => `/create${appendSearch(search)}`;
export const getRuleDetailsUrl = (detailName: string, search?: string) =>
`/rules/id/${detailName}${appendSearch(search)}`;
`/id/${detailName}${appendSearch(search)}`;
export const getEditRuleUrl = (detailName: string, search?: string) =>
`/rules/id/${detailName}/edit${appendSearch(search)}`;
`/id/${detailName}/edit${appendSearch(search)}`;

View file

@ -6,10 +6,10 @@
*/
import { HostsTableType } from '../../../hosts/store/model';
import { HOSTS_PATH } from '../../../../common/constants';
import { appendSearch } from './helpers';
export const getHostsUrl = (search?: string) => `${appendSearch(search)}`;
export const getHostsUrl = (search?: string) => `${HOSTS_PATH}${appendSearch(search)}`;
export const getTabsOnHostsUrl = (tabName: HostsTableType, search?: string) =>
`/${tabName}${appendSearch(search)}`;

View file

@ -9,8 +9,6 @@ import { isEmpty } from 'lodash/fp';
import { TimelineTypeLiteral } from '../../../../common/types/timeline';
import { appendSearch } from './helpers';
export const getTimelinesUrl = (search?: string) => `${appendSearch(search)}`;
export const getTimelineTabsUrl = (tabName: TimelineTypeLiteral, search?: string) =>
`/${tabName}${appendSearch(search)}`;

View file

@ -20,7 +20,7 @@ import React, { useMemo, useCallback } from 'react';
import { isNil } from 'lodash/fp';
import styled from 'styled-components';
import { IP_REPUTATION_LINKS_SETTING, APP_ID, CASES_APP_ID } from '../../../../common/constants';
import { IP_REPUTATION_LINKS_SETTING, APP_ID } from '../../../../common/constants';
import {
DefaultFieldRendererOverflow,
DEFAULT_MORE_MAX_HEIGHT,
@ -71,7 +71,8 @@ const HostDetailsLinkComponent: React.FC<{
const goToHostDetails = useCallback(
(ev) => {
ev.preventDefault();
navigateToApp(`${APP_ID}:${SecurityPageName.hosts}`, {
navigateToApp(APP_ID, {
deepLinkId: SecurityPageName.hosts,
path: getHostDetailsUrl(encodeURIComponent(hostName), search),
});
},
@ -142,7 +143,8 @@ const NetworkDetailsLinkComponent: React.FC<{
const goToNetworkDetails = useCallback(
(ev) => {
ev.preventDefault();
navigateToApp(`${APP_ID}:${SecurityPageName.network}`, {
navigateToApp(APP_ID, {
deepLinkId: SecurityPageName.network,
path: getNetworkDetailsUrl(encodeURIComponent(encodeIpv6(ip)), flowTarget, search),
});
},
@ -179,7 +181,8 @@ const CaseDetailsLinkComponent: React.FC<{
const goToCaseDetails = useCallback(
async (ev) => {
ev.preventDefault();
return navigateToApp(CASES_APP_ID, {
return navigateToApp(APP_ID, {
deepLinkId: SecurityPageName.case,
path: getCaseDetailsUrl({ id: detailName, search, subCaseId }),
});
},
@ -206,7 +209,8 @@ export const CreateCaseLink = React.memo<{ children: React.ReactNode }>(({ child
const goToCreateCase = useCallback(
async (ev) => {
ev.preventDefault();
return navigateToApp(CASES_APP_ID, {
return navigateToApp(APP_ID, {
deepLinkId: SecurityPageName.case,
path: getCreateCaseUrl(search),
});
},

View file

@ -8,95 +8,98 @@
import { parse, stringify } from 'query-string';
import React from 'react';
import { Redirect, Route, Switch, RouteComponentProps } from 'react-router-dom';
import { Redirect, Route, Switch, useRouteMatch } from 'react-router-dom';
import { addEntitiesToKql } from './add_entities_to_kql';
import { replaceKQLParts } from './replace_kql_parts';
import { emptyEntity, multipleEntities, getMultipleEntities } from './entity_helpers';
import { HostsTableType } from '../../../../hosts/store/model';
import { url as urlUtils } from '../../../../../../../../src/plugins/kibana_utils/public';
import { HOSTS_PATH } from '../../../../../common/constants';
interface QueryStringType {
'?_g': string;
query: string | null;
timerange: string | null;
}
type MlHostConditionalProps = Partial<RouteComponentProps<{}>> & { url: string };
export const MlHostConditionalContainer = React.memo(() => {
const { path } = useRouteMatch();
return (
<Switch>
<Route
strict
exact
path={path}
render={({ location }) => {
const queryStringDecoded = parse(location.search.substring(1), {
sort: false,
}) as Required<QueryStringType>;
export const MlHostConditionalContainer = React.memo<MlHostConditionalProps>(({ url }) => (
<Switch>
<Route
strict
exact
path={url}
render={({ location }) => {
const queryStringDecoded = parse(location.search.substring(1), {
sort: false,
}) as Required<QueryStringType>;
if (queryStringDecoded.query != null) {
queryStringDecoded.query = replaceKQLParts(queryStringDecoded.query);
}
const reEncoded = stringify(urlUtils.encodeQuery(queryStringDecoded), {
sort: false,
encode: false,
});
return <Redirect to={`?${reEncoded}`} />;
}}
/>
<Route
path={`${url}/:hostName`}
render={({
location,
match: {
params: { hostName },
},
}) => {
const queryStringDecoded = parse(location.search.substring(1), {
sort: false,
}) as Required<QueryStringType>;
if (queryStringDecoded.query != null) {
queryStringDecoded.query = replaceKQLParts(queryStringDecoded.query);
}
if (emptyEntity(hostName)) {
if (queryStringDecoded.query != null) {
queryStringDecoded.query = replaceKQLParts(queryStringDecoded.query);
}
const reEncoded = stringify(urlUtils.encodeQuery(queryStringDecoded), {
sort: false,
encode: false,
});
return <Redirect to={`/${HostsTableType.anomalies}?${reEncoded}`} />;
} else if (multipleEntities(hostName)) {
const hosts: string[] = getMultipleEntities(hostName);
queryStringDecoded.query = addEntitiesToKql(
['host.name'],
hosts,
queryStringDecoded.query || ''
);
const reEncoded = stringify(urlUtils.encodeQuery(queryStringDecoded), {
return <Redirect to={`${HOSTS_PATH}?${reEncoded}`} />;
}}
/>
<Route
path={`${path}/:hostName`}
render={({
location,
match: {
params: { hostName },
},
}) => {
const queryStringDecoded = parse(location.search.substring(1), {
sort: false,
encode: false,
});
}) as Required<QueryStringType>;
return <Redirect to={`/${HostsTableType.anomalies}?${reEncoded}`} />;
} else {
const reEncoded = stringify(urlUtils.encodeQuery(queryStringDecoded), {
sort: false,
encode: false,
});
if (queryStringDecoded.query != null) {
queryStringDecoded.query = replaceKQLParts(queryStringDecoded.query);
}
if (emptyEntity(hostName)) {
const reEncoded = stringify(urlUtils.encodeQuery(queryStringDecoded), {
sort: false,
encode: false,
});
return <Redirect to={`/${hostName}/${HostsTableType.anomalies}?${reEncoded}`} />;
}
}}
/>
<Route
path="/ml-hosts/"
render={({ location: { search = '' } }) => (
<Redirect from="/ml-hosts/" to={`/ml-hosts${search}`} />
)}
/>
</Switch>
));
return <Redirect to={`${HOSTS_PATH}/${HostsTableType.anomalies}?${reEncoded}`} />;
} else if (multipleEntities(hostName)) {
const hosts: string[] = getMultipleEntities(hostName);
queryStringDecoded.query = addEntitiesToKql(
['host.name'],
hosts,
queryStringDecoded.query || ''
);
const reEncoded = stringify(urlUtils.encodeQuery(queryStringDecoded), {
sort: false,
encode: false,
});
return <Redirect to={`${HOSTS_PATH}/${HostsTableType.anomalies}?${reEncoded}`} />;
} else {
const reEncoded = stringify(urlUtils.encodeQuery(queryStringDecoded), {
sort: false,
encode: false,
});
return (
<Redirect to={`${HOSTS_PATH}/${hostName}/${HostsTableType.anomalies}?${reEncoded}`} />
);
}
}}
/>
<Route
path={`${HOSTS_PATH}/ml-hosts/`}
render={({ location: { search = '' } }) => (
<Redirect from={`${HOSTS_PATH}/ml-hosts/`} to={`${HOSTS_PATH}/ml-hosts${search}`} />
)}
/>
</Switch>
);
});
MlHostConditionalContainer.displayName = 'MlHostConditionalContainer';

View file

@ -8,95 +8,102 @@
import { parse, stringify } from 'query-string';
import React from 'react';
import { Redirect, Route, Switch, RouteComponentProps } from 'react-router-dom';
import { Redirect, Route, Switch, useRouteMatch } from 'react-router-dom';
import { addEntitiesToKql } from './add_entities_to_kql';
import { replaceKQLParts } from './replace_kql_parts';
import { emptyEntity, getMultipleEntities, multipleEntities } from './entity_helpers';
import { url as urlUtils } from '../../../../../../../../src/plugins/kibana_utils/public';
import { NETWORK_PATH } from '../../../../../common/constants';
interface QueryStringType {
'?_g': string;
query: string | null;
timerange: string | null;
}
type MlNetworkConditionalProps = Partial<RouteComponentProps<{}>> & { url: string };
export const MlNetworkConditionalContainer = React.memo(() => {
const { path } = useRouteMatch();
return (
<Switch>
<Route
strict
exact
path={path}
render={({ location }) => {
const queryStringDecoded = parse(location.search.substring(1), {
sort: false,
}) as Required<QueryStringType>;
export const MlNetworkConditionalContainer = React.memo<MlNetworkConditionalProps>(({ url }) => (
<Switch>
<Route
strict
exact
path={url}
render={({ location }) => {
const queryStringDecoded = parse(location.search.substring(1), {
sort: false,
}) as Required<QueryStringType>;
if (queryStringDecoded.query != null) {
queryStringDecoded.query = replaceKQLParts(queryStringDecoded.query);
}
if (queryStringDecoded.query != null) {
queryStringDecoded.query = replaceKQLParts(queryStringDecoded.query);
}
const reEncoded = stringify(urlUtils.encodeQuery(queryStringDecoded), {
sort: false,
encode: false,
});
return <Redirect to={`?${reEncoded}`} />;
}}
/>
<Route
path={`${url}/ip/:ip`}
render={({
location,
match: {
params: { ip },
},
}) => {
const queryStringDecoded = parse(location.search.substring(1), {
sort: false,
}) as Required<QueryStringType>;
if (queryStringDecoded.query != null) {
queryStringDecoded.query = replaceKQLParts(queryStringDecoded.query);
}
if (emptyEntity(ip)) {
const reEncoded = stringify(urlUtils.encodeQuery(queryStringDecoded), {
sort: false,
encode: false,
});
return <Redirect to={`?${reEncoded}`} />;
} else if (multipleEntities(ip)) {
const ips: string[] = getMultipleEntities(ip);
queryStringDecoded.query = addEntitiesToKql(
['source.ip', 'destination.ip'],
ips,
queryStringDecoded.query || ''
);
const reEncoded = stringify(urlUtils.encodeQuery(queryStringDecoded), {
return <Redirect to={`${NETWORK_PATH}?${reEncoded}`} />;
}}
/>
<Route
path={`${path}/ip/:ip`}
render={({
location,
match: {
params: { ip },
},
}) => {
const queryStringDecoded = parse(location.search.substring(1), {
sort: false,
encode: false,
});
return <Redirect to={`?${reEncoded}`} />;
} else {
const reEncoded = stringify(urlUtils.encodeQuery(queryStringDecoded), {
sort: false,
encode: false,
});
return <Redirect to={`/ip/${ip}?${reEncoded}`} />;
}
}}
/>
<Route
path="/ml-network/"
render={({ location: { search = '' } }) => (
<Redirect from="/ml-network/" to={`/ml-network${search}`} />
)}
/>
</Switch>
));
}) as Required<QueryStringType>;
if (queryStringDecoded.query != null) {
queryStringDecoded.query = replaceKQLParts(queryStringDecoded.query);
}
if (emptyEntity(ip)) {
const reEncoded = stringify(urlUtils.encodeQuery(queryStringDecoded), {
sort: false,
encode: false,
});
return <Redirect to={`${NETWORK_PATH}?${reEncoded}`} />;
} else if (multipleEntities(ip)) {
const ips: string[] = getMultipleEntities(ip);
queryStringDecoded.query = addEntitiesToKql(
['source.ip', 'destination.ip'],
ips,
queryStringDecoded.query || ''
);
const reEncoded = stringify(urlUtils.encodeQuery(queryStringDecoded), {
sort: false,
encode: false,
});
return <Redirect to={`${NETWORK_PATH}?${reEncoded}`} />;
} else {
const reEncoded = stringify(urlUtils.encodeQuery(queryStringDecoded), {
sort: false,
encode: false,
});
return <Redirect to={`${NETWORK_PATH}/ip/${ip}?${reEncoded}`} />;
}
}}
/>
<Route
path={`${NETWORK_PATH}/ml-network/`}
render={({ location: { search = '' } }) => (
<Redirect
from={`${NETWORK_PATH}/ml-network/`}
to={{
pathname: `${NETWORK_PATH}/ml-network`,
search,
}}
/>
)}
/>
</Switch>
);
});
MlNetworkConditionalContainer.displayName = 'MlNetworkConditionalContainer';

View file

@ -73,6 +73,27 @@ const getMockObject = (
name: 'Timelines',
urlKey: 'timeline',
},
alerts: {
disabled: false,
href: '/app/security/alerts',
id: 'alerts',
name: 'Alerts',
urlKey: 'alerts',
},
exceptions: {
disabled: false,
href: '/app/security/exceptions',
id: 'exceptions',
name: 'Exceptions',
urlKey: 'exceptions',
},
rules: {
disabled: false,
href: '/app/security/rules',
id: 'rules',
name: 'Rules',
urlKey: 'rules',
},
},
pageName,
pathName,
@ -112,8 +133,10 @@ const getMockObject = (
});
// The string returned is different from what getUrlForApp returns, but does not matter for the purposes of this test.
const getUrlForAppMock = (appId: string, options?: { path?: string; absolute?: boolean }) =>
`${appId}${options?.path ?? ''}`;
const getUrlForAppMock = (
appId: string,
options?: { deepLinkId?: string; path?: string; absolute?: boolean }
) => `${appId}${options?.deepLinkId ? `/${options.deepLinkId}` : ''}${options?.path ?? ''}`;
describe('Navigation Breadcrumbs', () => {
const hostName = 'siem-kibana';
@ -130,12 +153,12 @@ describe('Navigation Breadcrumbs', () => {
);
expect(breadcrumbs).toEqual([
{
href: 'securitySolutionoverview',
href: 'securitySolution/overview',
text: 'Security',
},
{
href:
"securitySolution:hosts?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
"securitySolution/hosts?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
text: 'Hosts',
},
{
@ -151,11 +174,11 @@ describe('Navigation Breadcrumbs', () => {
getUrlForAppMock
);
expect(breadcrumbs).toEqual([
{ text: 'Security', href: 'securitySolutionoverview' },
{ text: 'Security', href: 'securitySolution/overview' },
{
text: 'Network',
href:
"securitySolution:network?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
"securitySolution/network?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
},
{
text: 'Flows',
@ -170,11 +193,11 @@ describe('Navigation Breadcrumbs', () => {
getUrlForAppMock
);
expect(breadcrumbs).toEqual([
{ text: 'Security', href: 'securitySolutionoverview' },
{ text: 'Security', href: 'securitySolution/overview' },
{
text: 'Timelines',
href:
"securitySolution:timelines?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
"securitySolution/timelines?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
},
]);
});
@ -185,16 +208,16 @@ describe('Navigation Breadcrumbs', () => {
getUrlForAppMock
);
expect(breadcrumbs).toEqual([
{ text: 'Security', href: 'securitySolutionoverview' },
{ text: 'Security', href: 'securitySolution/overview' },
{
text: 'Hosts',
href:
"securitySolution:hosts?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
"securitySolution/hosts?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
},
{
text: 'siem-kibana',
href:
"securitySolution:hosts/siem-kibana?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
"securitySolution/hosts/siem-kibana?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
},
{ text: 'Authentications', href: '' },
]);
@ -206,15 +229,15 @@ describe('Navigation Breadcrumbs', () => {
getUrlForAppMock
);
expect(breadcrumbs).toEqual([
{ text: 'Security', href: 'securitySolutionoverview' },
{ text: 'Security', href: 'securitySolution/overview' },
{
text: 'Network',
href:
"securitySolution:network?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
"securitySolution/network?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
},
{
text: ipv4,
href: `securitySolution:network/ip/${ipv4}/source?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`,
href: `securitySolution/network/ip/${ipv4}/source?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`,
},
{ text: 'Flows', href: '' },
]);
@ -226,45 +249,152 @@ describe('Navigation Breadcrumbs', () => {
getUrlForAppMock
);
expect(breadcrumbs).toEqual([
{ text: 'Security', href: 'securitySolutionoverview' },
{ text: 'Security', href: 'securitySolution/overview' },
{
text: 'Network',
href:
"securitySolution:network?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
"securitySolution/network?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
},
{
text: ipv6,
href: `securitySolution:network/ip/${ipv6Encoded}/source?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`,
href: `securitySolution/network/ip/${ipv6Encoded}/source?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`,
},
{ text: 'Flows', href: '' },
]);
});
test('should return Alerts breadcrumbs when supplied detection pathname', () => {
test('should return Alerts breadcrumbs when supplied alerts pathname', () => {
const breadcrumbs = getBreadcrumbsForRoute(
getMockObject('detections', '/', undefined),
getMockObject('alerts', '/alerts', undefined),
getUrlForAppMock
);
expect(breadcrumbs).toEqual([
{ text: 'Security', href: 'securitySolutionoverview' },
{ text: 'Security', href: 'securitySolution/overview' },
{
text: 'Detections',
href:
"securitySolution:detections?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
text: 'Alerts',
href: '',
},
]);
});
test('should return Exceptions breadcrumbs when supplied exceptions pathname', () => {
const breadcrumbs = getBreadcrumbsForRoute(
getMockObject('exceptions', '/exceptions', undefined),
getUrlForAppMock
);
expect(breadcrumbs).toEqual([
{ text: 'Security', href: 'securitySolution/overview' },
{
text: 'Exceptions',
href: '',
},
]);
});
test('should return Rules breadcrumbs when supplied rules pathname', () => {
const breadcrumbs = getBreadcrumbsForRoute(
getMockObject('rules', '/rules', undefined),
getUrlForAppMock
);
expect(breadcrumbs).toEqual([
{ text: 'Security', href: 'securitySolution/overview' },
{
text: 'Rules',
href:
"securitySolution/rules?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
},
]);
});
test('should return Rules breadcrumbs when supplied rules Creation pathname', () => {
const breadcrumbs = getBreadcrumbsForRoute(
getMockObject('rules', '/rules/create', undefined),
getUrlForAppMock
);
expect(breadcrumbs).toEqual([
{ text: 'Security', href: 'securitySolution/overview' },
{
text: 'Rules',
href:
"securitySolution/rules?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
},
{
text: 'Create',
href:
"securitySolution/rules/create?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
},
]);
});
test('should return Rules breadcrumbs when supplied rules Details pathname', () => {
const mockDetailName = '5a4a0460-d822-11eb-8962-bfd4aff0a9b3';
const mockRuleName = 'RULE_NAME';
const breadcrumbs = getBreadcrumbsForRoute(
{
...getMockObject('rules', `/rules/id/${mockDetailName}`, undefined),
detailName: mockDetailName,
state: {
ruleName: mockRuleName,
},
},
getUrlForAppMock
);
expect(breadcrumbs).toEqual([
{ text: 'Security', href: 'securitySolution/overview' },
{
text: 'Rules',
href:
"securitySolution/rules?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
},
{
text: mockRuleName,
href: `securitySolution/rules/id/${mockDetailName}?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`,
},
]);
});
test('should return Rules breadcrumbs when supplied rules Edit pathname', () => {
const mockDetailName = '5a4a0460-d822-11eb-8962-bfd4aff0a9b3';
const mockRuleName = 'RULE_NAME';
const breadcrumbs = getBreadcrumbsForRoute(
{
...getMockObject('rules', `/rules/id/${mockDetailName}/edit`, undefined),
detailName: mockDetailName,
state: {
ruleName: mockRuleName,
},
},
getUrlForAppMock
);
expect(breadcrumbs).toEqual([
{ text: 'Security', href: 'securitySolution/overview' },
{
text: 'Rules',
href:
"securitySolution/rules?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
},
{
text: 'RULE_NAME',
href: `securitySolution/rules/id/${mockDetailName}?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`,
},
{
text: 'Edit',
href: `securitySolution/rules/id/${mockDetailName}/edit?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`,
},
]);
});
test('should return Cases breadcrumbs when supplied case pathname', () => {
const breadcrumbs = getBreadcrumbsForRoute(
getMockObject('case', '/', undefined),
getUrlForAppMock
);
expect(breadcrumbs).toEqual([
{ text: 'Security', href: 'securitySolutionoverview' },
{ text: 'Security', href: 'securitySolution/overview' },
{
text: 'Cases',
href:
"securitySolution:case?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
"securitySolution/case?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
},
]);
});
@ -281,15 +411,15 @@ describe('Navigation Breadcrumbs', () => {
getUrlForAppMock
);
expect(breadcrumbs).toEqual([
{ text: 'Security', href: 'securitySolutionoverview' },
{ text: 'Security', href: 'securitySolution/overview' },
{
text: 'Cases',
href:
"securitySolution:case?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
"securitySolution/case?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
},
{
text: sampleCase.name,
href: `securitySolution:case/${sampleCase.id}?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`,
href: `securitySolution/case/${sampleCase.id}?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`,
},
]);
});
@ -299,10 +429,10 @@ describe('Navigation Breadcrumbs', () => {
getUrlForAppMock
);
expect(breadcrumbs).toEqual([
{ text: 'Security', href: 'securitySolutionoverview' },
{ text: 'Security', href: 'securitySolution/overview' },
{
text: 'Administration',
href: 'securitySolution:administration',
href: 'securitySolution/endpoints',
},
]);
});
@ -321,11 +451,11 @@ describe('Navigation Breadcrumbs', () => {
getUrlForAppMock
);
expect(breadcrumbs).toEqual([
{ text: 'Security', href: 'securitySolutionoverview' },
{ text: 'Security', href: 'securitySolution/overview' },
{
text: 'Timelines',
href:
"securitySolution:timelines?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))&timeline=(activeTab:query,graphEventId:GRAPH_EVENT_ID,id:TIMELINE_ID,isOpen:!f)",
"securitySolution/timelines?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))&timeline=(activeTab:query,graphEventId:GRAPH_EVENT_ID,id:TIMELINE_ID,isOpen:!f)",
},
]);
});
@ -333,20 +463,35 @@ describe('Navigation Breadcrumbs', () => {
describe('setBreadcrumbs()', () => {
test('should call chrome breadcrumb service with correct breadcrumbs', () => {
setBreadcrumbs(getMockObject('hosts', '/', hostName), chromeMock, getUrlForAppMock);
const navigateToUrlMock = jest.fn();
setBreadcrumbs(
getMockObject('hosts', '/', hostName),
chromeMock,
getUrlForAppMock,
navigateToUrlMock
);
expect(setBreadcrumbsMock).toBeCalledWith([
{ text: 'Security', href: 'securitySolutionoverview' },
{
expect.objectContaining({
text: 'Security',
href: 'securitySolution/overview',
onClick: expect.any(Function),
}),
expect.objectContaining({
text: 'Hosts',
href:
"securitySolution:hosts?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
},
{
"securitySolution/hosts?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
onClick: expect.any(Function),
}),
expect.objectContaining({
text: 'siem-kibana',
href:
"securitySolution:hosts/siem-kibana?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
"securitySolution/hosts/siem-kibana?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
onClick: expect.any(Function),
}),
{
text: 'Authentications',
href: '',
},
{ text: 'Authentications', href: '' },
]);
});
});

View file

@ -28,16 +28,29 @@ import { getAppOverviewUrl } from '../../link_to';
import { TabNavigationProps } from '../tab_navigation/types';
import { getSearch } from '../helpers';
import { GetUrlForApp, SearchNavTab } from '../types';
import { GetUrlForApp, NavigateToUrl, SearchNavTab } from '../types';
export const setBreadcrumbs = (
spyState: RouteSpyState & TabNavigationProps,
chrome: StartServices['chrome'],
getUrlForApp: GetUrlForApp
getUrlForApp: GetUrlForApp,
navigateToUrl: NavigateToUrl
) => {
const breadcrumbs = getBreadcrumbsForRoute(spyState, getUrlForApp);
if (breadcrumbs) {
chrome.setBreadcrumbs(breadcrumbs);
chrome.setBreadcrumbs(
breadcrumbs.map((breadcrumb) => ({
...breadcrumb,
...(breadcrumb.href && !breadcrumb.onClick
? {
onClick: (ev) => {
ev.preventDefault();
navigateToUrl(breadcrumb.href!);
},
}
: {}),
}))
);
}
};
@ -53,12 +66,12 @@ const isTimelinesRoutes = (spyState: RouteSpyState): spyState is TimelineRouteSp
const isCaseRoutes = (spyState: RouteSpyState): spyState is RouteSpyState =>
spyState != null && spyState.pageName === SecurityPageName.case;
const isAlertsRoutes = (spyState: RouteSpyState) =>
spyState != null && spyState.pageName === SecurityPageName.detections;
const isAdminRoutes = (spyState: RouteSpyState): spyState is AdministrationRouteSpyState =>
spyState != null && spyState.pageName === SecurityPageName.administration;
const isRulesRoutes = (spyState: RouteSpyState): spyState is AdministrationRouteSpyState =>
spyState != null && spyState.pageName === SecurityPageName.rules;
// eslint-disable-next-line complexity
export const getBreadcrumbsForRoute = (
objectParam: RouteSpyState & TabNavigationProps,
@ -69,7 +82,7 @@ export const getBreadcrumbsForRoute = (
// Sets `timeline.isOpen` to false in the state to avoid reopening the timeline on breadcrumb click. https://github.com/elastic/kibana/issues/100322
const object = { ...objectParam, timeline: { ...objectParam.timeline, isOpen: false } };
const overviewPath = getUrlForApp(APP_ID, { path: SecurityPageName.overview });
const overviewPath = getUrlForApp(APP_ID, { deepLinkId: SecurityPageName.overview });
const siemRootBreadcrumb: ChromeBreadcrumb = {
text: APP_NAME,
href: getAppOverviewUrl(overviewPath),
@ -80,6 +93,7 @@ export const getBreadcrumbsForRoute = (
if (spyState.tabName != null) {
urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)];
}
return [
siemRootBreadcrumb,
...getHostDetailsBreadcrumbs(
@ -110,8 +124,8 @@ export const getBreadcrumbsForRoute = (
),
];
}
if (isAlertsRoutes(spyState) && object.navTabs) {
const tempNav: SearchNavTab = { urlKey: 'detections', isDetailPage: false };
if (isRulesRoutes(spyState) && object.navTabs) {
const tempNav: SearchNavTab = { urlKey: SecurityPageName.rules, isDetailPage: false };
let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)];
if (spyState.tabName != null) {
urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)];
@ -129,6 +143,7 @@ export const getBreadcrumbsForRoute = (
),
];
}
if (isCaseRoutes(spyState) && object.navTabs) {
const tempNav: SearchNavTab = { urlKey: 'case', isDetailPage: false };
let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)];

View file

@ -32,7 +32,8 @@ jest.mock('./breadcrumbs', () => ({
setBreadcrumbs: jest.fn(),
}));
const mockGetUrlForApp = jest.fn();
jest.mock('../../lib/kibana', () => {
const mockNavigateToUrl = jest.fn();
jest.mock('../../lib/kibana/kibana_react', () => {
return {
useKibana: () => ({
services: {
@ -40,6 +41,7 @@ jest.mock('../../lib/kibana', () => {
application: {
navigateToApp: jest.fn(),
getUrlForApp: mockGetUrlForApp,
navigateToUrl: mockNavigateToUrl,
},
},
}),
@ -47,6 +49,13 @@ jest.mock('../../lib/kibana', () => {
});
jest.mock('../link_to');
jest.mock('react-router-dom', () => ({
useLocation: jest.fn(() => ({
search: '',
})),
useHistory: jest.fn(),
}));
describe('SIEM Navigation', () => {
const mockProps: TabNavigationComponentProps &
SecuritySolutionTabNavigationProps &
@ -97,57 +106,7 @@ describe('SIEM Navigation', () => {
1,
{
detailName: undefined,
navTabs: {
detections: {
disabled: false,
href: '/app/security/detections',
id: 'detections',
name: 'Detections',
urlKey: 'detections',
},
case: {
disabled: false,
href: '/app/security/cases',
id: 'case',
name: 'Cases',
urlKey: 'case',
},
administration: {
disabled: false,
href: '/app/security/administration',
id: 'administration',
name: 'Administration',
urlKey: 'administration',
},
hosts: {
disabled: false,
href: '/app/security/hosts',
id: 'hosts',
name: 'Hosts',
urlKey: 'host',
},
network: {
disabled: false,
href: '/app/security/network',
id: 'network',
name: 'Network',
urlKey: 'network',
},
overview: {
disabled: false,
href: '/app/security/overview',
id: 'overview',
name: 'Overview',
urlKey: 'overview',
},
timelines: {
disabled: false,
href: '/app/security/timelines',
id: 'timelines',
name: 'Timelines',
urlKey: 'timeline',
},
},
navTabs,
pageName: 'hosts',
pathName: '/',
search: '',
@ -188,7 +147,8 @@ describe('SIEM Navigation', () => {
},
},
undefined,
mockGetUrlForApp
mockGetUrlForApp,
mockNavigateToUrl
);
});
test('it calls setBreadcrumbs with correct path on update', () => {
@ -204,57 +164,7 @@ describe('SIEM Navigation', () => {
detailName: undefined,
filters: [],
flowTarget: undefined,
navTabs: {
detections: {
disabled: false,
href: '/app/security/detections',
id: 'detections',
name: 'Detections',
urlKey: 'detections',
},
case: {
disabled: false,
href: '/app/security/cases',
id: 'case',
name: 'Cases',
urlKey: 'case',
},
hosts: {
disabled: false,
href: '/app/security/hosts',
id: 'hosts',
name: 'Hosts',
urlKey: 'host',
},
administration: {
disabled: false,
href: '/app/security/administration',
id: 'administration',
name: 'Administration',
urlKey: 'administration',
},
network: {
disabled: false,
href: '/app/security/network',
id: 'network',
name: 'Network',
urlKey: 'network',
},
overview: {
disabled: false,
href: '/app/security/overview',
id: 'overview',
name: 'Overview',
urlKey: 'overview',
},
timelines: {
disabled: false,
href: '/app/security/timelines',
id: 'timelines',
name: 'Timelines',
urlKey: 'timeline',
},
},
navTabs,
pageName: 'network',
pathName: '/',
query: { language: 'kuery', query: '' },
@ -288,7 +198,8 @@ describe('SIEM Navigation', () => {
},
},
undefined,
mockGetUrlForApp
mockGetUrlForApp,
mockNavigateToUrl
);
});
});

View file

@ -39,7 +39,7 @@ export const TabNavigationComponent: React.FC<
}) => {
const {
chrome,
application: { getUrlForApp },
application: { getUrlForApp, navigateToUrl },
} = useKibana().services;
useEffect(() => {
@ -62,7 +62,8 @@ export const TabNavigationComponent: React.FC<
timerange: urlState.timerange,
},
chrome,
getUrlForApp
getUrlForApp,
navigateToUrl
);
}
}, [
@ -77,6 +78,7 @@ export const TabNavigationComponent: React.FC<
flowTarget,
tabName,
getUrlForApp,
navigateToUrl,
]);
return (

View file

@ -9,8 +9,6 @@ import { mount } from 'enzyme';
import React from 'react';
import { TimelineTabs } from '../../../../../common/types/timeline';
import { navTabs } from '../../../../app/home/home_navigations';
import { SecurityPageName } from '../../../../app/types';
import { navTabsHostDetails } from '../../../../hosts/pages/details/nav_tabs';
import { HostsTableType } from '../../../../hosts/store/model';
import { RouteSpyState } from '../../../utils/route/types';
@ -18,153 +16,107 @@ import { CONSTANTS } from '../../url_state/constants';
import { TabNavigationComponent } from './';
import { TabNavigationProps } from './types';
jest.mock('../../../lib/kibana');
jest.mock('../../link_to');
jest.mock('../../../lib/kibana/kibana_react', () => {
const originalModule = jest.requireActual('../../../../common/lib/kibana/kibana_react');
return {
...originalModule,
useKibana: jest.fn().mockReturnValue({
services: {
application: {
getUrlForApp: (appId: string, options?: { path?: string }) =>
`/app/${appId}${options?.path}`,
navigateToApp: jest.fn(),
},
},
}),
useUiSetting$: jest.fn().mockReturnValue([]),
};
});
const SEARCH_QUERY = '?search=test';
jest.mock('react-router-dom', () => {
const original = jest.requireActual('react-router-dom');
return {
...original,
useHistory: () => ({
push: jest.fn(),
}),
useLocation: jest.fn(() => ({
search: SEARCH_QUERY,
})),
};
});
describe('Tab Navigation', () => {
const pageName = SecurityPageName.hosts;
const hostName = 'siem-window';
const tabName = HostsTableType.authentications;
const pathName = `/${pageName}/${hostName}/${tabName}`;
const hostName = 'siem-window';
describe('Page Navigation', () => {
const mockProps: TabNavigationProps & RouteSpyState = {
pageName,
pathName,
detailName: undefined,
search: '',
tabName,
navTabs,
[CONSTANTS.timerange]: {
global: {
[CONSTANTS.timerange]: {
from: '2019-05-16T23:10:43.696Z',
fromStr: 'now-24h',
kind: 'relative',
to: '2019-05-17T23:10:43.697Z',
toStr: 'now',
},
linkTo: ['timeline'],
describe('Table Navigation', () => {
const mockHasMlUserPermissions = true;
const mockProps: TabNavigationProps & RouteSpyState = {
pageName: 'hosts',
pathName: '/hosts',
detailName: undefined,
search: '',
tabName: HostsTableType.authentications,
navTabs: navTabsHostDetails(hostName, mockHasMlUserPermissions),
[CONSTANTS.timerange]: {
global: {
[CONSTANTS.timerange]: {
from: '2019-05-16T23:10:43.696Z',
fromStr: 'now-24h',
kind: 'relative',
to: '2019-05-17T23:10:43.697Z',
toStr: 'now',
},
timeline: {
[CONSTANTS.timerange]: {
from: '2019-05-16T23:10:43.696Z',
fromStr: 'now-24h',
kind: 'relative',
to: '2019-05-17T23:10:43.697Z',
toStr: 'now',
},
linkTo: ['global'],
linkTo: ['timeline'],
},
timeline: {
[CONSTANTS.timerange]: {
from: '2019-05-16T23:10:43.696Z',
fromStr: 'now-24h',
kind: 'relative',
to: '2019-05-17T23:10:43.697Z',
toStr: 'now',
},
linkTo: ['global'],
},
[CONSTANTS.appQuery]: { query: 'host.name:"siem-es"', language: 'kuery' },
[CONSTANTS.filters]: [],
[CONSTANTS.sourcerer]: {},
[CONSTANTS.timeline]: {
activeTab: TimelineTabs.query,
id: '',
isOpen: false,
graphEventId: '',
},
};
test('it mounts with correct tab highlighted', () => {
const wrapper = mount(<TabNavigationComponent {...mockProps} />);
const hostsTab = wrapper.find('EuiTab[data-test-subj="navigation-hosts"]');
expect(hostsTab.prop('isSelected')).toBeTruthy();
});
test('it changes active tab when nav changes by props', () => {
const wrapper = mount(<TabNavigationComponent {...mockProps} />);
const networkTab = () => wrapper.find('EuiTab[data-test-subj="navigation-network"]').first();
expect(networkTab().prop('isSelected')).toBeFalsy();
wrapper.setProps({
pageName: 'network',
pathName: '/network',
tabName: undefined,
});
wrapper.update();
expect(networkTab().prop('isSelected')).toBeTruthy();
});
},
[CONSTANTS.appQuery]: { query: 'host.name:"siem-es"', language: 'kuery' },
[CONSTANTS.filters]: [],
[CONSTANTS.sourcerer]: {},
[CONSTANTS.timeline]: {
activeTab: TimelineTabs.query,
id: '',
isOpen: false,
graphEventId: '',
},
};
test('it mounts with correct tab highlighted', () => {
const wrapper = mount(<TabNavigationComponent {...mockProps} />);
const tableNavigationTab = wrapper.find(
`EuiTab[data-test-subj="navigation-${HostsTableType.authentications}"]`
);
expect(tableNavigationTab.prop('isSelected')).toBeTruthy();
});
test('it changes active tab when nav changes by props', () => {
const wrapper = mount(<TabNavigationComponent {...mockProps} />);
const tableNavigationTab = () =>
wrapper.find(`[data-test-subj="navigation-${HostsTableType.events}"]`).first();
expect(tableNavigationTab().prop('isSelected')).toBeFalsy();
wrapper.setProps({
tabName: HostsTableType.events,
});
wrapper.update();
expect(tableNavigationTab().prop('isSelected')).toBeTruthy();
});
test('it carries the url state in the link', () => {
const wrapper = mount(<TabNavigationComponent {...mockProps} />);
describe('Table Navigation', () => {
const mockHasMlUserPermissions = true;
const mockProps: TabNavigationProps & RouteSpyState = {
pageName: 'hosts',
pathName: '/hosts',
detailName: undefined,
search: '',
tabName: HostsTableType.authentications,
navTabs: navTabsHostDetails(hostName, mockHasMlUserPermissions),
[CONSTANTS.timerange]: {
global: {
[CONSTANTS.timerange]: {
from: '2019-05-16T23:10:43.696Z',
fromStr: 'now-24h',
kind: 'relative',
to: '2019-05-17T23:10:43.697Z',
toStr: 'now',
},
linkTo: ['timeline'],
},
timeline: {
[CONSTANTS.timerange]: {
from: '2019-05-16T23:10:43.696Z',
fromStr: 'now-24h',
kind: 'relative',
to: '2019-05-17T23:10:43.697Z',
toStr: 'now',
},
linkTo: ['global'],
},
},
[CONSTANTS.appQuery]: { query: 'host.name:"siem-es"', language: 'kuery' },
[CONSTANTS.filters]: [],
[CONSTANTS.sourcerer]: {},
[CONSTANTS.timeline]: {
activeTab: TimelineTabs.query,
id: '',
isOpen: false,
graphEventId: '',
},
};
test('it mounts with correct tab highlighted', () => {
const wrapper = mount(<TabNavigationComponent {...mockProps} />);
const tableNavigationTab = wrapper.find(
`EuiTab[data-test-subj="navigation-${HostsTableType.authentications}"]`
);
expect(tableNavigationTab.prop('isSelected')).toBeTruthy();
});
test('it changes active tab when nav changes by props', () => {
const wrapper = mount(<TabNavigationComponent {...mockProps} />);
const tableNavigationTab = () =>
wrapper.find(`[data-test-subj="navigation-${HostsTableType.events}"]`).first();
expect(tableNavigationTab().prop('isSelected')).toBeFalsy();
wrapper.setProps({
pageName: SecurityPageName.hosts,
pathName: `/${SecurityPageName.hosts}`,
tabName: HostsTableType.events,
});
wrapper.update();
expect(tableNavigationTab().prop('isSelected')).toBeTruthy();
});
test('it carries the url state in the link', () => {
const wrapper = mount(<TabNavigationComponent {...mockProps} />);
const firstTab = wrapper.find(
`EuiTab[data-test-subj="navigation-${HostsTableType.authentications}"]`
);
expect(firstTab.props().href).toBe('/siem-window/authentications');
});
const firstTab = wrapper.find(
`EuiTab[data-test-subj="navigation-${HostsTableType.authentications}"]`
);
expect(firstTab.props().href).toBe(
`/app/securitySolution/hosts/siem-window/authentications${SEARCH_QUERY}`
);
});
});

View file

@ -8,48 +8,35 @@
import { EuiTab, EuiTabs } from '@elastic/eui';
import { getOr } from 'lodash/fp';
import React, { useEffect, useState, useCallback, useMemo } from 'react';
import { useHistory } from 'react-router-dom';
import { useLocation } from 'react-router-dom';
import deepEqual from 'fast-deep-equal';
import { APP_ID } from '../../../../../common/constants';
import { useNavigation } from '../../../lib/kibana/hooks';
import { track, METRIC_TYPE, TELEMETRY_EVENT } from '../../../lib/telemetry';
import { getSearch } from '../helpers';
import { TabNavigationProps, TabNavigationItemProps } from './types';
import { useKibana } from '../../../lib/kibana';
import { SecurityPageName } from '../../../../app/types';
import { useFormatUrl } from '../../link_to';
const TabNavigationItemComponent = ({
disabled,
href,
hrefWithSearch,
id,
name,
isSelected,
pageId,
urlSearch,
}: TabNavigationItemProps) => {
const history = useHistory();
const { navigateToApp, getUrlForApp } = useKibana().services.application;
const { formatUrl } = useFormatUrl(((pageId ?? id) as unknown) as SecurityPageName);
const { getAppUrl, navigateTo } = useNavigation();
const handleClick = useCallback(
(ev) => {
ev.preventDefault();
if (id in SecurityPageName && pageId == null) {
navigateToApp(`${APP_ID}:${id}`, { path: urlSearch });
} else {
history.push(hrefWithSearch);
}
navigateTo({ path: hrefWithSearch });
track(METRIC_TYPE.CLICK, `${TELEMETRY_EVENT.TAB_CLICKED}${id}`);
},
[history, hrefWithSearch, id, navigateToApp, pageId, urlSearch]
[navigateTo, hrefWithSearch, id]
);
const appHref =
pageId != null
? formatUrl(href)
: getUrlForApp(`${APP_ID}:${id}`, {
path: urlSearch,
});
const appHref = getAppUrl({
path: hrefWithSearch,
});
return (
<EuiTab
data-href={appHref}
@ -68,28 +55,17 @@ const TabNavigationItem = React.memo(TabNavigationItemComponent);
export const TabNavigationComponent: React.FC<TabNavigationProps> = ({
display,
filters,
query,
navTabs,
pageName,
savedQuery,
sourcerer,
tabName,
timeline,
timerange,
}) => {
const mapLocationToTab = useCallback(
(): string =>
getOr(
'',
'id',
Object.values(navTabs).find(
(item) =>
(tabName === item.id && item.pageId != null) ||
(pageName === item.id && item.pageId == null)
)
Object.values(navTabs).find((item) => tabName === item.id)
),
[pageName, tabName, navTabs]
[tabName, navTabs]
);
const [selectedTabId, setSelectedTabId] = useState(mapLocationToTab());
useEffect(() => {
@ -100,38 +76,27 @@ export const TabNavigationComponent: React.FC<TabNavigationProps> = ({
}
// we do need navTabs in case the selectedTabId appears after initial load (ex. checking permissions for anomalies)
}, [pageName, tabName, navTabs, mapLocationToTab, selectedTabId]);
}, [tabName, navTabs, mapLocationToTab, selectedTabId]);
const { search } = useLocation();
const renderTabs = useMemo(
() =>
Object.values(navTabs).map((tab) => {
const isSelected = selectedTabId === tab.id;
const search = getSearch(tab, {
filters,
query,
savedQuery,
sourcerer,
timeline,
timerange,
});
const hrefWithSearch =
tab.href + getSearch(tab, { filters, query, savedQuery, sourcerer, timeline, timerange });
return (
<TabNavigationItem
key={`navigation-${tab.id}`}
id={tab.id}
href={tab.href}
hrefWithSearch={hrefWithSearch}
hrefWithSearch={tab.href + search}
name={tab.name}
disabled={tab.disabled}
pageId={tab.pageId}
isSelected={isSelected}
urlSearch={search}
/>
);
}),
[navTabs, selectedTabId, filters, query, savedQuery, sourcerer, timeline, timerange]
[navTabs, selectedTabId, search]
);
return <EuiTabs display={display}>{renderTabs}</EuiTabs>;
@ -143,15 +108,8 @@ export const TabNavigation = React.memo(
TabNavigationComponent,
(prevProps, nextProps) =>
prevProps.display === nextProps.display &&
prevProps.pageName === nextProps.pageName &&
prevProps.savedQuery === nextProps.savedQuery &&
prevProps.tabName === nextProps.tabName &&
deepEqual(prevProps.filters, nextProps.filters) &&
deepEqual(prevProps.query, nextProps.query) &&
deepEqual(prevProps.navTabs, nextProps.navTabs) &&
deepEqual(prevProps.sourcerer, nextProps.sourcerer) &&
deepEqual(prevProps.timeline, nextProps.timeline) &&
deepEqual(prevProps.timerange, nextProps.timerange)
deepEqual(prevProps.navTabs, nextProps.navTabs)
);
TabNavigation.displayName = 'TabNavigation';

View file

@ -27,12 +27,9 @@ export interface TabNavigationProps extends SecuritySolutionTabNavigationProps {
}
export interface TabNavigationItemProps {
href: string;
hrefWithSearch: string;
id: string;
disabled: boolean;
name: string;
isSelected: boolean;
urlSearch: string;
pageId?: string;
}

View file

@ -6,7 +6,7 @@
*/
import { UrlStateType } from '../url_state/constants';
import { SecurityPageName } from '../../../app/types';
import { SecurityPageName, SecurityPageGroupName } from '../../../app/types';
import { UrlState } from '../url_state/types';
import { SiemRouteType } from '../../utils/route/types';
@ -23,13 +23,25 @@ export interface TabNavigationComponentProps {
export type SearchNavTab = NavTab | { urlKey: UrlStateType; isDetailPage: boolean };
export interface NavGroupTab {
id: string;
name: string;
}
export type SecurityNavTabGroupKey =
| SecurityPageGroupName.detect
| SecurityPageGroupName.explore
| SecurityPageGroupName.investigate
| SecurityPageGroupName.manage;
export type NavTabGroups = Record<SecurityNavTabGroupKey, NavGroupTab>;
export interface NavTab {
id: string;
name: string;
href: string;
disabled: boolean;
urlKey: UrlStateType;
isDetailPage?: boolean;
urlKey?: UrlStateType;
pageId?: SecurityPageName;
}
@ -37,14 +49,21 @@ export type SiemNavTabKey =
| SecurityPageName.overview
| SecurityPageName.hosts
| SecurityPageName.network
| SecurityPageName.detections
| SecurityPageName.alerts
| SecurityPageName.rules
| SecurityPageName.exceptions
| SecurityPageName.timelines
| SecurityPageName.case
| SecurityPageName.administration;
| SecurityPageName.administration
| SecurityPageName.endpoints
| SecurityPageName.trustedApps
| SecurityPageName.eventFilters;
export type SiemNavTab = Record<SiemNavTabKey, NavTab>;
export type GetUrlForApp = (
appId: string,
options?: { path?: string; absolute?: boolean }
options?: { deepLinkId?: string; path?: string; absolute?: boolean }
) => string;
export type NavigateToUrl = (url: string) => void;

View file

@ -74,8 +74,8 @@ describe('useSecuritySolutionNavigation', () => {
services: {
application: {
navigateToApp: jest.fn(),
getUrlForApp: (appId: string, options?: { path?: string; absolute?: boolean }) =>
`${appId}${options?.path ?? ''}`,
getUrlForApp: (appId: string, options?: { path?: string; deepLinkId?: boolean }) =>
`${appId}/${options?.deepLinkId ?? ''}${options?.path ?? ''}`,
},
chrome: {
setBreadcrumbs: jest.fn(),
@ -97,67 +97,131 @@ describe('useSecuritySolutionNavigation', () => {
"id": "securitySolution",
"items": Array [
Object {
"data-href": "securitySolution: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-href": "securitySolution/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": "securitySolution: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)))",
"href": "securitySolution/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": "securitySolution:detections?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-detections",
"data-href": "securitySolution/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": "securitySolution:detections?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": "detections",
"href": "securitySolution/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": "Detections",
"name": "Alerts",
"onClick": [Function],
},
Object {
"data-href": "securitySolution: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-href": "securitySolution/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": "securitySolution/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": "securitySolution/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": "securitySolution/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": "Exceptions",
"onClick": [Function],
},
],
"name": "Detect",
},
Object {
"id": "explore",
"items": Array [
Object {
"data-href": "securitySolution/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": "securitySolution: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)))",
"href": "securitySolution/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": "securitySolution: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-href": "securitySolution/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": "securitySolution: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)))",
"href": "securitySolution/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": "securitySolution: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-href": "securitySolution/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": "securitySolution: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)))",
"href": "securitySolution/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": "securitySolution:administration",
"data-test-subj": "navigation-administration",
"data-href": "securitySolution/endpoints",
"data-test-subj": "navigation-endpoints",
"disabled": false,
"href": "securitySolution:administration",
"id": "administration",
"href": "securitySolution/endpoints",
"id": "endpoints",
"isSelected": false,
"name": "Administration",
"name": "Endpoints",
"onClick": [Function],
},
Object {
"data-href": "securitySolution/trusted_apps",
"data-test-subj": "navigation-trusted_apps",
"disabled": false,
"href": "securitySolution/trusted_apps",
"id": "trusted_apps",
"isSelected": false,
"name": "Trusted Applications",
"onClick": [Function],
},
Object {
"data-href": "securitySolution/event_filters",
"data-test-subj": "navigation-event_filters",
"disabled": false,
"href": "securitySolution/event_filters",
"id": "event_filters",
"isSelected": false,
"name": "Event Filters",
"onClick": [Function],
},
],
"name": "",
"name": "Manage",
},
],
"name": "Security",
@ -177,15 +241,15 @@ describe('useSecuritySolutionNavigation', () => {
useSecuritySolutionNavigation()
);
const caseNavItem = (result.current?.items || [])[0].items?.find(
const caseNavItem = (result.current?.items || [])[3].items?.find(
(item) => item['data-test-subj'] === 'navigation-case'
);
expect(caseNavItem).toMatchInlineSnapshot(`
Object {
"data-href": "securitySolution:case?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-href": "securitySolution/case?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-case",
"disabled": false,
"href": "securitySolution:case?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)))",
"href": "securitySolution/case?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": "case",
"isSelected": false,
"name": "Cases",
@ -204,7 +268,7 @@ describe('useSecuritySolutionNavigation', () => {
useSecuritySolutionNavigation()
);
const caseNavItem = (result.current?.items || [])[0].items?.find(
const caseNavItem = (result.current?.items || [])[3].items?.find(
(item) => item['data-test-subj'] === 'navigation-case'
);
expect(caseNavItem).toBeFalsy();

View file

@ -6,15 +6,13 @@
*/
import { useEffect } from 'react';
import { pickBy } from 'lodash/fp';
import { usePrimaryNavigation } from './use_primary_navigation';
import { useGetUserCasesPermissions, useKibana } from '../../../lib/kibana';
import { useKibana } from '../../../lib/kibana';
import { setBreadcrumbs } from '../breadcrumbs';
import { makeMapStateToProps } from '../../url_state/helpers';
import { useRouteSpy } from '../../../utils/route/use_route_spy';
import { navTabs } from '../../../../app/home/home_navigations';
import { useDeepEqualSelector } from '../../../hooks/use_selector';
import { SecurityPageName } from '../../../../../common/constants';
/**
* @description - This hook provides the structure necessary by the KibanaPageTemplate for rendering the primary security_solution side navigation.
@ -26,7 +24,7 @@ export const useSecuritySolutionNavigation = () => {
const { urlState } = useDeepEqualSelector(urlMapState);
const {
chrome,
application: { getUrlForApp },
application: { getUrlForApp, navigateToUrl },
} = useKibana().services;
const { detailName, flowTarget, pageName, pathName, search, state, tabName } = routeProps;
@ -51,7 +49,8 @@ export const useSecuritySolutionNavigation = () => {
timerange: urlState.timerange,
},
chrome,
getUrlForApp
getUrlForApp,
navigateToUrl
);
}
}, [
@ -65,25 +64,17 @@ export const useSecuritySolutionNavigation = () => {
flowTarget,
tabName,
getUrlForApp,
navigateToUrl,
]);
const hasCasesReadPermissions = useGetUserCasesPermissions()?.read;
// build a list of tabs to exclude
const tabsToExclude = new Set<string>([
...(!hasCasesReadPermissions ? [SecurityPageName.case] : []),
]);
// include the tab if it is not in the set of excluded ones
const tabsToDisplay = pickBy((_, key) => !tabsToExclude.has(key), navTabs);
return usePrimaryNavigation({
query: urlState.query,
filters: urlState.filters,
navTabs: tabsToDisplay,
navTabs,
pageName,
sourcerer: urlState.sourcerer,
savedQuery: urlState.savedQuery,
tabName,
timeline: urlState.timeline,
timerange: urlState.timerange,
});

View file

@ -12,4 +12,4 @@ export type PrimaryNavigationItemsProps = Omit<
'pathName' | 'pageName' | 'tabName'
> & { selectedTabId: string };
export type PrimaryNavigationProps = Omit<TabNavigationProps, 'pathName' | 'tabName'>;
export type PrimaryNavigationProps = Omit<TabNavigationProps, 'pathName'>;

View file

@ -5,62 +5,85 @@
* 2.0.
*/
import React from 'react';
import React, { useCallback, useMemo } from 'react';
import { EuiSideNavItemType } from '@elastic/eui/src/components/side_nav/side_nav_types';
import { navTabGroups } from '../../../../app/home/home_navigations';
import { APP_ID } from '../../../../../common/constants';
import { track, METRIC_TYPE, TELEMETRY_EVENT } from '../../../lib/telemetry';
import { getSearch } from '../helpers';
import { PrimaryNavigationItemsProps } from './types';
import { useKibana } from '../../../lib/kibana';
import { useGetUserCasesPermissions, useKibana } from '../../../lib/kibana';
import { NavTab } from '../types';
export const usePrimaryNavigationItems = ({
filters,
navTabs,
query,
savedQuery,
selectedTabId,
sourcerer,
timeline,
timerange,
}: PrimaryNavigationItemsProps) => {
...urlStateProps
}: PrimaryNavigationItemsProps): Array<EuiSideNavItemType<{}>> => {
const { navigateToApp, getUrlForApp } = useKibana().services.application;
const navItems = Object.values(navTabs).map((tab) => {
const { id, name, disabled } = tab;
const isSelected = selectedTabId === id;
const urlSearch = getSearch(tab, {
filters,
query,
savedQuery,
sourcerer,
timeline,
timerange,
});
const getSideNav = useCallback(
(tab: NavTab) => {
const { id, name, disabled } = tab;
const isSelected = selectedTabId === id;
const urlSearch = getSearch(tab, urlStateProps);
const handleClick = (ev: React.MouseEvent) => {
ev.preventDefault();
navigateToApp(`${APP_ID}:${id}`, { path: urlSearch });
track(METRIC_TYPE.CLICK, `${TELEMETRY_EVENT.TAB_CLICKED}${id}`);
};
const handleClick = (ev: React.MouseEvent) => {
ev.preventDefault();
navigateToApp(APP_ID, { deepLinkId: id, path: urlSearch });
};
const appHref = getUrlForApp(`${APP_ID}:${id}`, { path: urlSearch });
const appHref = getUrlForApp(APP_ID, { deepLinkId: id, path: urlSearch });
return {
'data-href': appHref,
'data-test-subj': `navigation-${id}`,
disabled,
href: appHref,
id,
isSelected,
name,
onClick: handleClick,
};
});
return {
'data-href': appHref,
'data-test-subj': `navigation-${id}`,
disabled,
href: appHref,
id,
isSelected,
name,
onClick: handleClick,
};
},
[getUrlForApp, navigateToApp, selectedTabId, urlStateProps]
);
const navItemsToDisplay = usePrimaryNavigationItemsToDisplay(navTabs);
return useMemo(
() =>
navItemsToDisplay.map((item) => ({
...item,
items: item.items.map((t: NavTab) => getSideNav(t)),
})),
[getSideNav, navItemsToDisplay]
);
};
function usePrimaryNavigationItemsToDisplay(navTabs: Record<string, NavTab>) {
const hasCasesReadPermissions = useGetUserCasesPermissions()?.read;
return [
{
id: APP_ID, // TODO: When separating into sub-sections (detect, explore, investigate). Those names can also serve as the section id
items: navItems,
id: APP_ID,
name: '',
items: [navTabs.overview],
},
{
...navTabGroups.detect,
items: [navTabs.alerts, navTabs.rules, navTabs.exceptions],
},
{
...navTabGroups.explore,
items: [navTabs.hosts, navTabs.network],
},
{
...navTabGroups.investigate,
items: hasCasesReadPermissions ? [navTabs.timelines, navTabs.case] : [navTabs.timelines],
},
{
...navTabGroups.manage,
items: [navTabs.endpoints, navTabs.trusted_apps, navTabs.event_filters],
},
];
};
}

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { getOr } from 'lodash/fp';
import { useEffect, useState, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
@ -24,17 +23,13 @@ export const usePrimaryNavigation = ({
pageName,
savedQuery,
sourcerer,
tabName,
timeline,
timerange,
}: PrimaryNavigationProps): KibanaPageTemplateProps['solutionNav'] => {
const mapLocationToTab = useCallback(
(): string =>
getOr(
'',
'id',
Object.values(navTabs).find((item) => pageName === item.id && item.pageId == null)
),
[pageName, navTabs]
(): string => ((tabName && navTabs[tabName]) || navTabs[pageName])?.id ?? '',
[pageName, tabName, navTabs]
);
const [selectedTabId, setSelectedTabId] = useState(mapLocationToTab());
@ -50,11 +45,11 @@ export const usePrimaryNavigation = ({
}, [pageName, navTabs, mapLocationToTab, selectedTabId]);
const navItems = usePrimaryNavigationItems({
filters,
navTabs,
selectedTabId,
filters,
query,
savedQuery,
selectedTabId,
sourcerer,
timeline,
timerange,

View file

@ -7,9 +7,9 @@
export enum CONSTANTS {
appQuery = 'query',
alertsPage = 'alerts.page',
caseDetails = 'case.details',
casePage = 'case.page',
detectionsPage = 'detections.page',
filters = 'filters',
hostsDetails = 'hosts.details',
hostsPage = 'hosts.page',
@ -27,7 +27,9 @@ export enum CONSTANTS {
export type UrlStateType =
| 'case'
| 'detections'
| 'alerts'
| 'rules'
| 'exceptions'
| 'host'
| 'network'
| 'overview'

View file

@ -92,8 +92,12 @@ export const getUrlType = (pageName: string): UrlStateType => {
return 'host';
} else if (pageName === SecurityPageName.network) {
return 'network';
} else if (pageName === SecurityPageName.detections) {
return 'detections';
} else if (pageName === SecurityPageName.alerts) {
return 'alerts';
} else if (pageName === SecurityPageName.rules) {
return 'rules';
} else if (pageName === SecurityPageName.exceptions) {
return 'exceptions';
} else if (pageName === SecurityPageName.timelines) {
return 'timeline';
} else if (pageName === SecurityPageName.case) {

View file

@ -170,7 +170,7 @@ describe('UrlStateContainer', () => {
});
});
describe('After Initialization, keep Relative Date up to date for global only on detections page', () => {
describe('After Initialization, keep Relative Date up to date for global only on alerts page', () => {
test.each(testCases)(
'%o',
async (page, namespaceLower, namespaceUpper, examplePath, type, pageName, detailName) => {
@ -196,7 +196,7 @@ describe('UrlStateContainer', () => {
});
wrapper.update();
if (CONSTANTS.detectionsPage === page) {
if (CONSTANTS.alertsPage === page) {
await waitFor(() => {
expect(mockSetRelativeRangeDatePicker.mock.calls[3][0]).toEqual({
from: '2020-01-01T00:00:00.000Z',

View file

@ -45,7 +45,7 @@ export const dispatchSetInitialStateFromUrl = (
const sourcererState = decodeRisonUrlState<SourcererScopePatterns>(newUrlStateString);
if (sourcererState != null) {
const activeScopes: SourcererScopeName[] = Object.keys(sourcererState).filter(
(key) => !(key === SourcererScopeName.default && pageName === SecurityPageName.detections)
(key) => !(key === SourcererScopeName.default && pageName === SecurityPageName.alerts)
) as SourcererScopeName[];
activeScopes.forEach((scope) =>
dispatch(

View file

@ -34,7 +34,23 @@ export const ALL_URL_STATE_KEYS: KeyUrlState[] = [
];
export const URL_STATE_KEYS: Record<UrlStateType, KeyUrlState[]> = {
detections: [
alerts: [
CONSTANTS.appQuery,
CONSTANTS.filters,
CONSTANTS.savedQuery,
CONSTANTS.sourcerer,
CONSTANTS.timerange,
CONSTANTS.timeline,
],
rules: [
CONSTANTS.appQuery,
CONSTANTS.filters,
CONSTANTS.savedQuery,
CONSTANTS.sourcerer,
CONSTANTS.timerange,
CONSTANTS.timeline,
],
exceptions: [
CONSTANTS.appQuery,
CONSTANTS.filters,
CONSTANTS.savedQuery,
@ -88,7 +104,7 @@ export const URL_STATE_KEYS: Record<UrlStateType, KeyUrlState[]> = {
export type LocationTypes =
| CONSTANTS.caseDetails
| CONSTANTS.casePage
| CONSTANTS.detectionsPage
| CONSTANTS.alertsPage
| CONSTANTS.hostsDetails
| CONSTANTS.hostsPage
| CONSTANTS.networkDetails

View file

@ -221,7 +221,7 @@ export const useUrlStateHooks = ({
}
});
} else if (pathName !== prevProps.pathName) {
handleInitialize(type, pageName === SecurityPageName.detections);
handleInitialize(type, pageName === SecurityPageName.alerts);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isInitializing, history, pathName, pageName, prevProps, urlState]);

View file

@ -158,8 +158,10 @@ export const useFetchIndex = (
next: (response) => {
if (isCompleteResponse(response)) {
const stringifyIndices = response.indicesExist.sort().join();
previousIndexesName.current = response.indicesExist;
setLoading(false);
setState({
browserFields: getBrowserFields(stringifyIndices, response.indexFields),
docValueFields: getDocValueFields(stringifyIndices, response.indexFields),
@ -167,6 +169,7 @@ export const useFetchIndex = (
indexExists: response.indicesExist.length > 0,
indexPatterns: getIndexFields(stringifyIndices, response.indexFields),
});
searchSubscription$.current.unsubscribe();
} else if (isErrorResponse(response)) {
setLoading(false);
@ -187,7 +190,7 @@ export const useFetchIndex = (
abortCtrl.current.abort();
asyncSearch();
},
[data.search, addError, addWarning, onlyCheckIfIndicesExist]
[data.search, addError, addWarning, onlyCheckIfIndicesExist, setLoading, setState]
);
useEffect(() => {

View file

@ -37,7 +37,7 @@ export const useNavigateToAppEventHandler = <S = unknown>(
options?: NavigateToAppHandlerOptions<S>
): EventHandlerCallback => {
const { services } = useKibana();
const { path, state, onClick } = options || {};
const { path, state, onClick, deepLinkId } = options || {};
return useCallback(
(ev) => {
try {
@ -70,8 +70,8 @@ export const useNavigateToAppEventHandler = <S = unknown>(
}
ev.preventDefault();
services.application.navigateToApp(appId, { path, state });
services.application.navigateToApp(appId, { deepLinkId, path, state });
},
[appId, onClick, path, services.application, state]
[appId, deepLinkId, onClick, path, services.application, state]
);
};

View file

@ -17,6 +17,8 @@ import {
createStartServicesMock,
createWithKibanaMock,
} from '../kibana_react.mock';
import { APP_ID } from '../../../../../common/constants';
const mockStartServicesMock = createStartServicesMock();
export const KibanaServices = { get: jest.fn(), getKibanaVersion: jest.fn(() => '8.0.0') };
export const useKibana = jest.fn().mockReturnValue({
@ -60,3 +62,10 @@ export const useCurrentUser = jest.fn();
export const withKibana = jest.fn(createWithKibanaMock());
export const KibanaContextProvider = jest.fn(createKibanaContextProviderMock());
export const useGetUserCasesPermissions = jest.fn();
export const useAppUrl = jest.fn().mockReturnValue({
getAppUrl: jest
.fn()
.mockImplementation(({ appId = APP_ID, ...options }) =>
mockStartServicesMock.application.getUrlForApp(appId, options)
),
});

View file

@ -12,7 +12,7 @@ import { i18n } from '@kbn/i18n';
import { camelCase, isArray, isObject } from 'lodash';
import { set } from '@elastic/safer-lodash-set';
import { DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ } from '../../../../common/constants';
import { APP_ID, DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ } from '../../../../common/constants';
import { errorToToaster, useStateToaster } from '../../components/toasters';
import { AuthenticatedUser } from '../../../../../security/common/model';
import { StartServices } from '../../../types';
@ -160,3 +160,57 @@ export const useGetUserCasesPermissions = () => {
return casesPermissions;
};
/**
* Returns a full URL to the provided page path by using
* kibana's `getUrlForApp()`
*/
export const useAppUrl = () => {
const { getUrlForApp } = useKibana().services.application;
const getAppUrl = useCallback(
({ appId = APP_ID, ...options }: { appId?: string; deepLinkId?: string; path?: string }) =>
getUrlForApp(appId, options),
[getUrlForApp]
);
return { getAppUrl };
};
/**
* Navigate to any app using kibana's `navigateToApp()`
* or by url using `navigateToUrl()`
*/
export const useNavigateTo = () => {
const { navigateToApp, navigateToUrl } = useKibana().services.application;
const navigateTo = useCallback(
({
url,
appId = APP_ID,
...options
}: {
url?: string;
appId?: string;
deepLinkId?: string;
path?: string;
}) => {
if (url) {
navigateToUrl(url);
} else {
navigateToApp(appId, options);
}
},
[navigateToApp, navigateToUrl]
);
return { navigateTo };
};
/**
* Returns navigateTo and getAppUrl navigation hooks
*
*/
export const useNavigation = () => {
const { navigateTo } = useNavigateTo();
const { getAppUrl } = useAppUrl();
return { navigateTo, getAppUrl };
};

View file

@ -6,9 +6,10 @@
*/
import React from 'react';
import { createMemoryHistory } from 'history';
import { createMemoryHistory, MemoryHistory } from 'history';
import { render as reactRender, RenderOptions, RenderResult } from '@testing-library/react';
import { Action, Reducer, Store } from 'redux';
import { AppDeepLink } from 'kibana/public';
import { coreMock } from '../../../../../../../src/core/public/mocks';
import { StartPlugins, StartServices } from '../../../types';
import { depsStartMock } from './dependencies_start_mock';
@ -21,10 +22,10 @@ import { createStartServicesMock } from '../../lib/kibana/kibana_react.mock';
import { SUB_PLUGINS_REDUCER, mockGlobalState, createSecuritySolutionStorageMock } from '..';
import { ExperimentalFeatures } from '../../../../common/experimental_features';
import { PLUGIN_ID } from '../../../../../fleet/common';
import { APP_ID } from '../../../../common/constants';
import { APP_ID, APP_PATH } from '../../../../common/constants';
import { KibanaContextProvider, KibanaServices } from '../../lib/kibana';
import { MANAGEMENT_APP_ID } from '../../../management/common/constants';
import { fleetGetPackageListHttpMock } from '../../../management/pages/endpoint_hosts/mocks';
import { getDeepLinks } from '../../../app/deep_links';
type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult;
@ -93,7 +94,7 @@ const experimentalFeaturesReducer: Reducer<State['app'], UpdateExperimentalFeatu
*/
export const createAppRootMockRenderer = (): AppContextTestRender => {
const history = createMemoryHistory<never>();
const coreStart = createCoreStartMock();
const coreStart = createCoreStartMock(history);
const depsStart = depsStartMock();
const middlewareSpy = createSpyMiddleware();
const { storage } = createSecuritySolutionStorageMock();
@ -166,26 +167,56 @@ export const createAppRootMockRenderer = (): AppContextTestRender => {
};
};
const createCoreStartMock = (): ReturnType<typeof coreMock.createStart> => {
const createCoreStartMock = (
history: MemoryHistory<never>
): ReturnType<typeof coreMock.createStart> => {
const coreStart = coreMock.createStart({ basePath: '/mock' });
const deepLinkPaths = getDeepLinkPaths(getDeepLinks());
// Mock the certain APP Ids returned by `application.getUrlForApp()`
coreStart.application.getUrlForApp.mockImplementation((appId) => {
coreStart.application.getUrlForApp.mockImplementation((appId, { deepLinkId, path } = {}) => {
switch (appId) {
case PLUGIN_ID:
return '/app/fleet';
case APP_ID:
return '/app/security';
case MANAGEMENT_APP_ID:
return '/app/security/administration';
return `${APP_PATH}${
deepLinkId && deepLinkPaths[deepLinkId] ? deepLinkPaths[deepLinkId] : ''
}${path ?? ''}`;
default:
return `${appId} not mocked!`;
}
});
coreStart.application.navigateToApp.mockImplementation((appId, { deepLinkId, path } = {}) => {
if (appId === APP_ID) {
history.push(
`${deepLinkId && deepLinkPaths[deepLinkId] ? deepLinkPaths[deepLinkId] : ''}${path ?? ''}`
);
}
return Promise.resolve();
});
coreStart.application.navigateToUrl.mockImplementation((url) => {
history.push(url.replace(APP_PATH, ''));
return Promise.resolve();
});
return coreStart;
};
const getDeepLinkPaths = (deepLinks: AppDeepLink[]): Record<string, string> => {
return deepLinks.reduce((result: Record<string, string>, deepLink) => {
if (deepLink.path) {
result[deepLink.id] = deepLink.path;
}
if (deepLink.deepLinks) {
return { ...result, ...getDeepLinkPaths(deepLink.deepLinks) };
}
return result;
}, {});
};
const applyDefaultCoreHttpMocks = (http: AppContextTestRender['coreStart']['http']) => {
// Need to mock getting the endpoint package from the fleet API because it is used as soon
// as the store middleware for Endpoint list is initialized, thus mocking it here would avoid

View file

@ -12,6 +12,8 @@ import { shallow, mount } from 'enzyme';
import '../../../common/mock/match_media';
import { esQuery } from '../../../../../../../src/plugins/data/public';
import { TestProviders } from '../../../common/mock';
import { SecurityPageName } from '../../../app/types';
import { AlertsHistogramPanel, buildCombinedQueries, parseCombinedQueries } from './index';
import * as helpers from './helpers';
@ -83,7 +85,10 @@ describe('AlertsHistogramPanel', () => {
preventDefault: jest.fn(),
});
expect(mockNavigateToApp).toBeCalledWith('securitySolution:detections', { path: '' });
expect(mockNavigateToApp).toBeCalledWith('securitySolution', {
deepLinkId: SecurityPageName.alerts,
path: '',
});
});
});

View file

@ -152,7 +152,7 @@ export const AlertsHistogramPanel = memo<AlertsHistogramPanelProps>(
});
const kibana = useKibana();
const { navigateToApp } = kibana.services.application;
const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.detections);
const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.alerts);
const totalAlerts = useMemo(
() =>
@ -175,7 +175,8 @@ export const AlertsHistogramPanel = memo<AlertsHistogramPanelProps>(
const goToDetectionEngine = useCallback(
(ev) => {
ev.preventDefault();
navigateToApp(`${APP_ID}:${SecurityPageName.detections}`, {
navigateToApp(APP_ID, {
deepLinkId: SecurityPageName.alerts,
path: getDetectionEngineUrl(urlSearch),
});
},

View file

@ -27,6 +27,7 @@ jest.mock('react-router-dom', () => {
});
jest.mock('../../../../common/components/link_to');
jest.mock('../../../../common/lib/kibana');
jest.mock('../../../containers/detection_engine/rules/api', () => ({
getPrePackagedRulesStatus: jest.fn().mockResolvedValue({
@ -52,11 +53,14 @@ describe('PrePackagedRulesPrompt', () => {
let appToastsMock: jest.Mocked<ReturnType<typeof useAppToastsMock.create>>;
beforeEach(() => {
jest.resetAllMocks();
appToastsMock = useAppToastsMock.create();
(useAppToasts as jest.Mock).mockReturnValue(appToastsMock);
});
afterEach(() => {
jest.clearAllMocks();
});
it('renders correctly', () => {
const wrapper = shallow(<PrePackagedRulesPrompt {...props} />);

View file

@ -9,7 +9,6 @@ import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React, { memo, useCallback, useMemo } from 'react';
import styled from 'styled-components';
import { useHistory } from 'react-router-dom';
import { getCreateRuleUrl } from '../../../../common/components/link_to/redirect_to_detection_engine';
import * as i18n from './translations';
import { LinkButton } from '../../../../common/components/links';
@ -17,6 +16,8 @@ import { SecurityPageName } from '../../../../app/types';
import { useFormatUrl } from '../../../../common/components/link_to';
import { usePrePackagedRules } from '../../../containers/detection_engine/rules';
import { useUserData } from '../../user_info';
import { APP_ID } from '../../../../../common/constants';
import { useKibana } from '../../../../common/lib/kibana';
const EmptyPrompt = styled(EuiEmptyPrompt)`
align-self: center; /* Corrects horizontal centering in IE11 */
@ -35,18 +36,18 @@ const PrePackagedRulesPromptComponent: React.FC<PrePackagedRulesPromptProps> = (
loading = false,
userHasPermissions = false,
}) => {
const history = useHistory();
const handlePreBuiltCreation = useCallback(() => {
createPrePackagedRules();
}, [createPrePackagedRules]);
const { formatUrl } = useFormatUrl(SecurityPageName.detections);
const { formatUrl } = useFormatUrl(SecurityPageName.rules);
const { navigateToApp } = useKibana().services.application;
const goToCreateRule = useCallback(
(ev) => {
ev.preventDefault();
history.push(getCreateRuleUrl());
navigateToApp(APP_ID, { deepLinkId: SecurityPageName.rules, path: getCreateRuleUrl() });
},
[history]
[navigateToApp]
);
const [

Some files were not shown because too many files have changed in this diff Show more