diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts index 1fa7d8e846c9d..65857f02c883d 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts @@ -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, diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 85d6bb391cc2f..86fffd212af65 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -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": { diff --git a/x-pack/plugins/cases/public/components/recent_cases/no_cases/index.tsx b/x-pack/plugins/cases/public/components/recent_cases/no_cases/index.tsx index a5b90943a219a..2d60cc1b223ad 100644 --- a/x-pack/plugins/cases/public/components/recent_cases/no_cases/index.tsx +++ b/x-pack/plugins/cases/public/components/recent_cases/no_cases/index.tsx @@ -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 ? ( <> {i18n.NO_CASES} - {` ${i18n.START_A_NEW_CASE}`} + >{` ${i18n.START_A_NEW_CASE}`} {'!'} ) : ( diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index d59d7e7b7da4f..3fb32856a1ef1 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -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 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 enum SecurityPageGroupName { + detect = 'detect', + explore = 'explore', + investigate = 'investigate', + manage = 'manage', +} -export const DETECTIONS_SUB_PLUGIN_ID = `${APP_ID}:${SecurityPageName.detections}`; +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 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 = [ diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/attach_timeline.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/attach_timeline.spec.ts index 3f3209b52120e..b8477d5b08280 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases/attach_timeline.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases/attach_timeline.spec.ts @@ -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 () { diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts index d5d8cb7d9af20..8f9ce8028ac77 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts @@ -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(); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_detection_callouts_index_outdated.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_detection_callouts_index_outdated.spec.ts index 1c6c604b84fbb..fb0b96c977e32 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_detection_callouts_index_outdated.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_detection_callouts_index_outdated.spec.ts @@ -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', () => { diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/attach_to_case.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/attach_to_case.spec.ts index 932f1ceac61e8..6cc5d2443e784 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/attach_to_case.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/attach_to_case.spec.ts @@ -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); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/closing.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/closing.spec.ts index 741f05129f9c4..6ae23733d6434 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/closing.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/closing.spec.ts @@ -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); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/in_progress.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/in_progress.spec.ts index b4f890e4d8dbf..cb8694d5c35af 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/in_progress.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/in_progress.spec.ts @@ -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); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/investigate_in_timeline.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/investigate_in_timeline.spec.ts index d705cb652d2ea..115118b6762d9 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/investigate_in_timeline.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/investigate_in_timeline.spec.ts @@ -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); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/missing_privileges_callout.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/missing_privileges_callout.spec.ts index 87a3dc8474876..20a863e742efd 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/missing_privileges_callout.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/missing_privileges_callout.spec.ts @@ -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', () => { diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/opening.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/opening.spec.ts index bc907dccd0a04..6cbc82b93f446 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/opening.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/opening.spec.ts @@ -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); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts index 8210c7c6d8b20..5f9175476795c 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/custom_query_rule.spec.ts @@ -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'); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts index b38796cca373d..337e2a8ec5033 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts @@ -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(); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/export_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/export_rule.spec.ts index cbd37dec13232..1de636010f967 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/export_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/export_rule.spec.ts @@ -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'); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts index bc8cf0137fa83..c2e8a92474814 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts @@ -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(); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/machine_learning_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/machine_learning_rule.spec.ts index 65dde40bbd76b..2d869b314b67c 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/machine_learning_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/machine_learning_rule.spec.ts @@ -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(); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts index f9f1ca14c8164..a791cc293c1f0 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/override.spec.ts @@ -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(); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts index 74e1d082ae410..b259c0f1d9e33 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/prebuilt_rules.spec.ts @@ -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(); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts index bf5c281a43e39..7d42ea533a9ae 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts @@ -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'); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts index 0f4095372f92a..ad71d54eb2a7a 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts @@ -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(); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_modal.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_modal.spec.ts index dee921b0c668a..a4b929f7d8e1d 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_modal.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_modal.spec.ts @@ -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(); diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_table.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_table.spec.ts index fdc8a268ca368..83277075b35cc 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_table.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_table.spec.ts @@ -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`); diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/from_alert.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/from_alert.spec.ts index e36809380df86..4918de7488ddd 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/from_alert.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/from_alert.spec.ts @@ -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(); diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/from_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/from_rule.spec.ts index e0d7e5a32edfd..ea8988456d8b3 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/from_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/from_rule.spec.ts @@ -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(); diff --git a/x-pack/plugins/security_solution/cypress/integration/header/navigation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/header/navigation.spec.ts index 3caddb86d6725..2079e8e47d479 100644 --- a/x-pack/plugins/security_solution/cypress/integration/header/navigation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/header/navigation.spec.ts @@ -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); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/urls/compatibility.spec.ts b/x-pack/plugins/security_solution/cypress/integration/urls/compatibility.spec.ts index b956eb64a04e5..bbbd6037d3862 100644 --- a/x-pack/plugins/security_solution/cypress/integration/urls/compatibility.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/urls/compatibility.spec.ts @@ -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); + }); + + 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); + }); - cy.url().should('include', '/security/detections'); + 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', () => { diff --git a/x-pack/plugins/security_solution/cypress/integration/urls/state.spec.ts b/x-pack/plugins/security_solution/cypress/integration/urls/state.spec.ts index 48dc6faf8524e..f2b644e8d054c 100644 --- a/x-pack/plugins/security_solution/cypress/integration/urls/state.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/urls/state.spec.ts @@ -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) diff --git a/x-pack/plugins/security_solution/cypress/integration/value_lists/value_lists.spec.ts b/x-pack/plugins/security_solution/cypress/integration/value_lists/value_lists.spec.ts index 0f545136d1c07..a7cc412a920c0 100644 --- a/x-pack/plugins/security_solution/cypress/integration/value_lists/value_lists.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/value_lists/value_lists.spec.ts @@ -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(); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/exceptions.ts b/x-pack/plugins/security_solution/cypress/screens/exceptions.ts index 2479b76cf1de4..bd6d3b7887206 100644 --- a/x-pack/plugins/security_solution/cypress/screens/exceptions.ts +++ b/x-pack/plugins/security_solution/cypress/screens/exceptions.ts @@ -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"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/kibana_navigation.ts b/x-pack/plugins/security_solution/cypress/screens/kibana_navigation.ts index 7a63357aabaf0..36b870598eff4 100644 --- a/x-pack/plugins/security_solution/cypress/screens/kibana_navigation.ts +++ b/x-pack/plugins/security_solution/cypress/screens/kibana_navigation.ts @@ -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"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/security_header.ts b/x-pack/plugins/security_solution/cypress/screens/security_header.ts index a3d5b714cdb3f..3573d78bfcf8a 100644 --- a/x-pack/plugins/security_solution/cypress/screens/security_header.ts +++ b/x-pack/plugins/security_solution/cypress/screens/security_header.ts @@ -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"]'; diff --git a/x-pack/plugins/security_solution/cypress/urls/navigation.ts b/x-pack/plugins/security_solution/cypress/urls/navigation.ts index 2beed9e8ec0b7..879f16f0b7e0e 100644 --- a/x-pack/plugins/security_solution/cypress/urls/navigation.ts +++ b/x-pack/plugins/security_solution/cypress/urls/navigation.ts @@ -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'; diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json index e26f0d9b65bfa..8601f312f1786 100644 --- a/x-pack/plugins/security_solution/kibana.json +++ b/x-pack/plugins/security_solution/kibana.json @@ -36,5 +36,13 @@ ], "server": true, "ui": true, - "requiredBundles": ["esUiShared", "fleet", "kibanaUtils", "kibanaReact", "lists", "ml"] + "requiredBundles": [ + "esUiShared", + "fleet", + "kibanaUtils", + "kibanaReact", + "usageCollection", + "lists", + "ml" + ] } diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts new file mode 100644 index 0000000000000..f125218b68c09 --- /dev/null +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts @@ -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(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.ts new file mode 100644 index 0000000000000..cbaa789d47489 --- /dev/null +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.ts @@ -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; +}) { + 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, + })); +} diff --git a/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx b/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx index 98ff11423ce01..85e41a5cc12ca 100644 --- a/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx @@ -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(); - unmount = mount(element); - return unmount; + return mount(element); }); return () => { portalNode.unmount(); - unmount(); + setHeaderActionMenu(undefined); }; }, [portalNode, setHeaderActionMenu]); return ( - {window.location.pathname.includes(APP_DETECTIONS_PATH) && ( + {isDetectionsPath(pathname) && ( @@ -61,7 +65,7 @@ export const GlobalHeader = React.memo( {BUTTON_ADD_DATA} diff --git a/x-pack/plugins/security_solution/public/app/home/home_navigations.ts b/x-pack/plugins/security_solution/public/app/home/home_navigations.ts new file mode 100644 index 0000000000000..271eea47840dc --- /dev/null +++ b/x-pack/plugins/security_solution/public/app/home/home_navigations.ts @@ -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, + }, +}; diff --git a/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx b/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx deleted file mode 100644 index 8358e2f9377b8..0000000000000 --- a/x-pack/plugins/security_solution/public/app/home/home_navigations.tsx +++ /dev/null @@ -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, - }, -}; diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx index 17a6fab103d6f..d16c35a832e6b 100644 --- a/x-pack/plugins/security_solution/public/app/home/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/index.tsx @@ -5,7 +5,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 = ({ onAppLeave, setHeaderActionMenu, }) => { - const { application } = useKibana().services; - const subPluginId = useRef(''); - - 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 diff --git a/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx b/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx index 08ebbeaee55d4..a2f1ed8c115d6 100644 --- a/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/template_wrapper/bottom_bar/index.tsx @@ -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(''); - 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 ? ( diff --git a/x-pack/plugins/security_solution/public/app/index.tsx b/x-pack/plugins/security_solution/public/app/index.tsx index 194f119e35478..81437ec9ec6f6 100644 --- a/x-pack/plugins/security_solution/public/app/index.tsx +++ b/x-pack/plugins/security_solution/public/app/index.tsx @@ -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( - + + + {[ + ...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) => ( + + ))} + + + + + + + + + , element ); diff --git a/x-pack/plugins/security_solution/public/app/search/index.test.ts b/x-pack/plugins/security_solution/public/app/search/index.test.ts deleted file mode 100644 index 328395f9b85c9..0000000000000 --- a/x-pack/plugins/security_solution/public/app/search/index.test.ts +++ /dev/null @@ -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); - } - }); -}); diff --git a/x-pack/plugins/security_solution/public/app/search/index.ts b/x-pack/plugins/security_solution/public/app/search/index.ts deleted file mode 100644 index 93d931fc4d137..0000000000000 --- a/x-pack/plugins/security_solution/public/app/search/index.ts +++ /dev/null @@ -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 { - 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, - licenseType?: LicenseType -) { - if (appUpdater !== undefined) { - appUpdater.next(() => ({ ...getDeepLinksAndKeywords(subPluginName, licenseType) })); - } -} diff --git a/x-pack/plugins/security_solution/public/app/translations.ts b/x-pack/plugins/security_solution/public/app/translations.ts index 80ddc19add3fd..edbab928a5c69 100644 --- a/x-pack/plugins/security_solution/public/app/translations.ts +++ b/x-pack/plugins/security_solution/public/app/translations.ts @@ -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', +}); diff --git a/x-pack/plugins/security_solution/public/app/types.ts b/x-pack/plugins/security_solution/public/app/types.ts index 77d5b99e1c3a3..62a61828830be 100644 --- a/x-pack/plugins/security_solution/public/app/types.ts +++ b/x-pack/plugins/security_solution/public/app/types.ts @@ -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; - 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 { initialState: Record; @@ -42,8 +44,10 @@ export interface SecuritySubPluginStore middleware?: Array>>>; } +export type SecuritySubPluginRoutes = RouteProps[]; + export interface SecuritySubPlugin { - SubPluginRoutes: React.FC; + routes: SecuritySubPluginRoutes; storageTimelines?: Pick; } @@ -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. diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx index aa84f639c4577..3c788e0553079 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx @@ -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(({ 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(({ 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(({ 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 }), }); }, diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx index dec2d409b020d..5474fcb47d87e 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx @@ -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 ?? ''), }); }, diff --git a/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx index 0b6e98e1badcc..42579c6fbc0ac 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx @@ -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, }) ); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx index dfd53ae5cc0b0..5e2b7e27fb1e5 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx @@ -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] diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx index 02047c774ca6f..efcc95c1a7782 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.test.tsx @@ -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, }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx index 7025bff1ce49a..f0f56d2497e88 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx @@ -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 = ({ 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 = ({ const goToCreateCase = useCallback( async (ev) => { ev.preventDefault(); - return navigateToApp(CASES_APP_ID, { + return navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.case, path: getCreateCaseUrl(urlSearch), }); }, diff --git a/x-pack/plugins/security_solution/public/cases/index.ts b/x-pack/plugins/security_solution/public/cases/index.ts index a520932b48660..631276a6bf6b4 100644 --- a/x-pack/plugins/security_solution/public/cases/index.ts +++ b/x-pack/plugins/security_solution/public/cases/index.ts @@ -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, }; } } diff --git a/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx b/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx index f6bb27b7b7104..e8680b148f940 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx @@ -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 ? ( <> diff --git a/x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx b/x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx index d3f235a5da7dc..13a549b6babc9 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/configure_cases.tsx @@ -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), }); } diff --git a/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx b/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx index 6c88c4afb6395..e46e5c2074f05 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/create_case.tsx @@ -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), }); } diff --git a/x-pack/plugins/security_solution/public/cases/pages/index.tsx b/x-pack/plugins/security_solution/public/cases/pages/index.tsx index fca19cf5c70a7..0faff93bb708c 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/index.tsx @@ -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 = () => { - + diff --git a/x-pack/plugins/security_solution/public/cases/pages/utils.ts b/x-pack/plugins/security_solution/public/cases/pages/utils.ts index 1c848190cbef5..968712009e110 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/utils.ts +++ b/x-pack/plugins/security_solution/public/cases/pages/utils.ts @@ -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 }), }), }, diff --git a/x-pack/plugins/security_solution/public/cases/routes.tsx b/x-pack/plugins/security_solution/public/cases/routes.tsx index c937631e9474f..8ea30e60379ca 100644 --- a/x-pack/plugins/security_solution/public/cases/routes.tsx +++ b/x-pack/plugins/security_solution/public/cases/routes.tsx @@ -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 = () => ( - - - - - } /> - +export const CasesRoutes = () => ( + + + ); + +export const routes: SecuritySubPluginRoutes = [ + { + path: CASES_PATH, + render: CasesRoutes, + }, +]; diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/link_to_app.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/link_to_app.tsx index da8ef269352c7..201738d3293b2 100644 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/link_to_app.tsx +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/link_to_app.tsx @@ -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( - ({ 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 ( <> diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/route_capture.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/route_capture.tsx index 7a7c812f48528..a5e0c90402df4 100644 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/route_capture.tsx +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/route_capture.tsx @@ -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 }); diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/header_page/index.test.tsx index 8a1748de582c4..2c42353daee75 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_page/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_page/index.test.tsx @@ -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', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx index 75453f2d759fb..dc8e19249b6be 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx @@ -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 = ({ 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 = ({ {backOptions.text} @@ -128,7 +131,6 @@ const HeaderPageComponent: React.FC = ({ )} {!backOptions && backComponent && <>{backComponent}} - {titleNode || ( ) => 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 }; }; diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_detection_engine.tsx b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_detection_engine.tsx index 3a9837c605bdd..b66d923cf0a15 100644 --- a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_detection_engine.tsx @@ -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)}`; diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_hosts.tsx b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_hosts.tsx index a043aeb195820..62057260b6383 100644 --- a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_hosts.tsx +++ b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_hosts.tsx @@ -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)}`; diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_timelines.tsx b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_timelines.tsx index adbc2e3a9b670..ecc14781f7005 100644 --- a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_timelines.tsx +++ b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_timelines.tsx @@ -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)}`; diff --git a/x-pack/plugins/security_solution/public/common/components/links/index.tsx b/x-pack/plugins/security_solution/public/common/components/links/index.tsx index 13888c98c4243..0b6b77aab00e4 100644 --- a/x-pack/plugins/security_solution/public/common/components/links/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/links/index.tsx @@ -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), }); }, diff --git a/x-pack/plugins/security_solution/public/common/components/ml/conditional_links/ml_host_conditional_container.tsx b/x-pack/plugins/security_solution/public/common/components/ml/conditional_links/ml_host_conditional_container.tsx index 048be383441ec..f92512cec5c72 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/conditional_links/ml_host_conditional_container.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/conditional_links/ml_host_conditional_container.tsx @@ -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<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)) { - const reEncoded = stringify(urlUtils.encodeQuery(queryStringDecoded), { +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, - encode: false, - }); + }) as Required<QueryStringType>; - return <Redirect to={`/${HostsTableType.anomalies}?${reEncoded}`} />; - } else if (multipleEntities(hostName)) { - const hosts: string[] = getMultipleEntities(hostName); - queryStringDecoded.query = addEntitiesToKql( - ['host.name'], - hosts, - queryStringDecoded.query || '' - ); + 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 { - 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>; + + 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={`${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={`/${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}/${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'; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/conditional_links/ml_network_conditional_container.tsx b/x-pack/plugins/security_solution/public/common/components/ml/conditional_links/ml_network_conditional_container.tsx index 81327937e7c4c..a144898b4d95c 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/conditional_links/ml_network_conditional_container.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/conditional_links/ml_network_conditional_container.tsx @@ -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<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); - } - - 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>; +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>; - if (queryStringDecoded.query != null) { - queryStringDecoded.query = replaceKQLParts(queryStringDecoded.query); - } + 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), { - sort: false, - encode: false, - }); - return <Redirect to={`?${reEncoded}`} />; - } else { - 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={`/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'; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts index c869df6ad388e..6789d8e1d4524 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts @@ -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('alerts', '/alerts', undefined), + getUrlForAppMock + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolution/overview' }, + { + 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('detections', '/', undefined), + { + ...getMockObject('rules', `/rules/id/${mockDetailName}`, undefined), + detailName: mockDetailName, + state: { + ruleName: mockRuleName, + }, + }, getUrlForAppMock ); expect(breadcrumbs).toEqual([ - { text: 'Security', href: 'securitySolutionoverview' }, + { text: 'Security', href: 'securitySolution/overview' }, { - text: 'Detections', + text: 'Rules', 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)))", + "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: '' }, ]); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts index a09945f705c58..4578e16dc5540 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts @@ -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)]; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx index c75b38e03acb4..5bb0805acc378 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx @@ -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 ); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx index 233b4b2cb1d02..71a5309937fff 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx @@ -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 ( diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx index 3e66a024c7bd0..18dd07a99824e 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx @@ -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'], - }, - 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'], +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'], }, - [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(); - }); - }); - - 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', }, - 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: '', + linkTo: ['global'], }, - }; - test('it mounts with correct tab highlighted', () => { - const wrapper = mount(<TabNavigationComponent {...mockProps} />); - const tableNavigationTab = wrapper.find( - `EuiTab[data-test-subj="navigation-${HostsTableType.authentications}"]` - ); + }, + [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'); + 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} />); + + const firstTab = wrapper.find( + `EuiTab[data-test-subj="navigation-${HostsTableType.authentications}"]` + ); + expect(firstTab.props().href).toBe( + `/app/securitySolution/hosts/siem-window/authentications${SEARCH_QUERY}` + ); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.tsx index 92596945a4769..2ca0d878078aa 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.tsx @@ -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'; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/types.ts b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/types.ts index 53565d79e6948..c99d50698db2b 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/types.ts @@ -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; } diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/types.ts b/x-pack/plugins/security_solution/public/common/components/navigation/types.ts index 1c317700b1d15..1b1b3c9af4bfc 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/types.ts @@ -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; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx index ef00bef841305..7e211a2e95152 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx @@ -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/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/alerts?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "id": "alerts", + "isSelected": false, + "name": "Alerts", + "onClick": [Function], + }, Object { - "data-href": "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/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: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/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": "Detections", + "name": "Rules", "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/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(); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.tsx index f2aee86912dd7..39c6885e8dff5 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.tsx @@ -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, }); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/types.ts b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/types.ts index f639b8a37f0da..f2c68f881528d 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/types.ts @@ -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'>; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx index 42ca7f4c65460..e04ec7727a08f 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx @@ -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], }, ]; -}; +} diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_primary_navigation.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_primary_navigation.tsx index 390f44b48b0b1..c1abcc1177c80 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_primary_navigation.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_primary_navigation.tsx @@ -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, diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts b/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts index 6149300f68e36..6107b61638888 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/constants.ts @@ -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' diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts index a5fbaf9ebb76b..8908b83fc9b56 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts @@ -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) { diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx index 2157b21179f15..b40799895e8a2 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx @@ -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', diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx index 8a7c6bcb4a9b5..9fc2e24221bcb 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx @@ -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( diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/types.ts b/x-pack/plugins/security_solution/public/common/components/url_state/types.ts index 1a8d512d211e6..63511c54d28db 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/types.ts @@ -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 diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx index 7785fa6af2569..10d586c2d7441 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx @@ -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]); diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx index 3bc92dafd351f..f619e6565b06b 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx @@ -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(() => { diff --git a/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_navigate_to_app_event_handler.ts b/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_navigate_to_app_event_handler.ts index 862bb43a08d38..148deda9aec76 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_navigate_to_app_event_handler.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_navigate_to_app_event_handler.ts @@ -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] ); }; diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts index 09c3d2537e272..61ce5a8238b52 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/__mocks__/index.ts @@ -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) + ), +}); diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts index 4a2caefba1b97..1b05c6a857263 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/hooks.ts @@ -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 }; +}; diff --git a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx index 3e582ee9b20c8..44a100e27e95b 100644 --- a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx @@ -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 diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx index 19d1e9c219191..31d1ce6d41153 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx @@ -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: '', + }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx index d766104e356eb..8a328c14726c9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.tsx @@ -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), }); }, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx index cbdfe5b246aff..880817af856f8 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx @@ -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} />); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx index 56875bcc4f88c..5688b4065ab76 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.tsx @@ -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 [ diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.test.tsx index 3a27469ba2539..c545de7fd8d7d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.test.tsx @@ -16,6 +16,20 @@ import { import { RuleActionsOverflow } from './index'; import { mockRule } from '../../../pages/detection_engine/rules/all/__mocks__/mock'; +jest.mock('../../../../common/lib/kibana', () => { + const actual = jest.requireActual('../../../../common/lib/kibana'); + return { + ...actual, + useKibana: jest.fn().mockReturnValue({ + services: { + application: { + navigateToApp: jest.fn(), + }, + }, + }), + }; +}); + jest.mock('react-router-dom', () => ({ useHistory: () => ({ push: jest.fn(), @@ -28,21 +42,14 @@ jest.mock('../../../pages/detection_engine/rules/all/actions', () => ({ editRuleAction: jest.fn(), })); -jest.mock('../../../../common/lib/kibana', () => { - return { - KibanaServices: { - get: () => ({ - http: { fetch: jest.fn() }, - }), - }, - }; -}); - const duplicateRulesActionMock = duplicateRulesAction as jest.Mock; const flushPromises = () => new Promise(setImmediate); describe('RuleActionsOverflow', () => { afterEach(() => { + jest.clearAllMocks(); + }); + afterAll(() => { jest.resetAllMocks(); }); @@ -229,7 +236,7 @@ describe('RuleActionsOverflow', () => { await flushPromises(); expect(duplicateRulesAction).toHaveBeenCalled(); - expect(editRuleAction).toHaveBeenCalledWith(ruleDuplicate, expect.anything()); + expect(editRuleAction).toHaveBeenCalledWith(ruleDuplicate.id, expect.anything()); }); describe('rules details export rule', () => { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx index e0841824d512f..2146123deafd5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx @@ -30,6 +30,7 @@ import { import { getRulesUrl } from '../../../../common/components/link_to/redirect_to_detection_engine'; import { getToolTipContent } from '../../../../common/utils/privileges'; import { useBoolState } from '../../../../common/hooks/use_bool_state'; +import { useKibana } from '../../../../common/lib/kibana'; const MyEuiButtonIcon = styled(EuiButtonIcon)` &.euiButtonIcon { @@ -58,6 +59,7 @@ const RuleActionsOverflowComponent = ({ }: RuleActionsOverflowComponentProps) => { const [isPopoverOpen, , closePopover, togglePopover] = useBoolState(); const history = useHistory(); + const { navigateToApp } = useKibana().services.application; const [, dispatchToaster] = useStateToaster(); const onRuleDeletedCallback = useCallback(() => { @@ -82,7 +84,7 @@ const RuleActionsOverflowComponent = ({ dispatchToaster ); if (createdRules?.length) { - editRuleAction(createdRules[0], history); + editRuleAction(createdRules[0].id, navigateToApp); } }} > @@ -123,7 +125,7 @@ const RuleActionsOverflowComponent = ({ canDuplicateRuleWithActions, closePopover, dispatchToaster, - history, + navigateToApp, onRuleDeletedCallback, rule, userHasPermissions, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx index 162f86c543308..6e21987db1668 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx @@ -302,7 +302,6 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({ ), [threatBrowserFields, threatIndexPatternsLoading, threatIndexPatterns, indexPatterns] ); - return isReadOnlyView ? ( <StepContentWrapper data-test-subj="definitionRule" addPadding={addPadding}> <StepRuleDescription @@ -343,7 +342,6 @@ const StepDefineRuleComponent: FC<StepDefineRuleProps> = ({ 'data-test-subj': 'detectionEngineStepDefineRuleIndices', euiFieldProps: { fullWidth: true, - isDisabled: isLoading, placeholder: '', }, }} diff --git a/x-pack/plugins/security_solution/public/detections/index.ts b/x-pack/plugins/security_solution/public/detections/index.ts index b6b26fde73edb..ca35efe4d9294 100644 --- a/x-pack/plugins/security_solution/public/detections/index.ts +++ b/x-pack/plugins/security_solution/public/detections/index.ts @@ -8,10 +8,10 @@ import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { getTimelinesInStorageByIds } from '../timelines/containers/local_storage'; import { TimelineIdLiteral, TimelineId } from '../../common/types/timeline'; -import { AlertsRoutes } from './routes'; +import { routes } from './routes'; import { SecuritySubPlugin } from '../app/types'; -const DETECTIONS_TIMELINE_IDS: TimelineIdLiteral[] = [ +export const DETECTIONS_TIMELINE_IDS: TimelineIdLiteral[] = [ TimelineId.detectionsRulesDetailsPage, TimelineId.detectionsPage, ]; @@ -21,10 +21,10 @@ export class Detections { public start(storage: Storage): SecuritySubPlugin { return { - SubPluginRoutes: AlertsRoutes, storageTimelines: { timelineById: getTimelinesInStorageByIds(storage, DETECTIONS_TIMELINE_IDS), }, + routes, }; } } diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index 0c12d8256d66d..f52b09e2d62b4 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -10,10 +10,8 @@ import styled from 'styled-components'; import { noop } from 'lodash/fp'; import React, { useCallback, useMemo, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; -import { useHistory } from 'react-router-dom'; -import { isTab } from '../../../../../timelines/public'; import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; - +import { isTab } from '../../../../../timelines/public'; import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector'; import { SecurityPageName } from '../../../app/types'; import { TimelineId } from '../../../../common/types/timeline'; @@ -25,7 +23,6 @@ import { SiemSearchBar } from '../../../common/components/search_bar'; import { SecuritySolutionPageWrapper } from '../../../common/components/page_wrapper'; import { inputsSelectors } from '../../../common/store/inputs'; import { setAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions'; -import { SpyRoute } from '../../../common/utils/route/spy_routes'; import { useAlertInfo } from '../../components/alerts_info'; import { AlertsTable } from '../../components/alerts_table'; import { NoApiIntegrationKeyCallOut } from '../../components/callouts/no_api_integration_callout'; @@ -59,6 +56,7 @@ import { useSourcererScope } from '../../../common/containers/sourcerer'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; import { NeedAdminForUpdateRulesCallOut } from '../../components/callouts/need_admin_for_update_callout'; import { MissingPrivilegesCallOut } from '../../components/callouts/missing_privileges_callout'; +import { useKibana } from '../../../common/lib/kibana'; /** * Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space. @@ -103,12 +101,12 @@ const DetectionEnginePageComponent = () => { loading: listsConfigLoading, needsConfiguration: needsListsConfiguration, } = useListsConfig(); - const history = useHistory(); const [lastAlerts] = useAlertInfo({}); - const { formatUrl } = useFormatUrl(SecurityPageName.detections); + const { formatUrl } = useFormatUrl(SecurityPageName.rules); const [showBuildingBlockAlerts, setShowBuildingBlockAlerts] = useState(false); const [showOnlyThreatIndicatorAlerts, setShowOnlyThreatIndicatorAlerts] = useState(false); const loading = userInfoLoading || listsConfigLoading; + const { navigateToUrl } = useKibana().services.application; const updateDateRangeCallback = useCallback<UpdateDateRange>( ({ x }) => { @@ -130,9 +128,9 @@ const DetectionEnginePageComponent = () => { const goToRules = useCallback( (ev) => { ev.preventDefault(); - history.push(getRulesUrl()); + navigateToUrl(formatUrl(getRulesUrl())); }, - [history] + [formatUrl, navigateToUrl] ); const alertsHistogramDefaultFilters = useMemo( @@ -288,7 +286,6 @@ const DetectionEnginePageComponent = () => { <OverviewEmpty /> </SecuritySolutionPageWrapper> )} - <SpyRoute pageName={SecurityPageName.detections} /> </> ); }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.test.tsx deleted file mode 100644 index 57e23452806b6..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.test.tsx +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { shallow } from 'enzyme'; - -import '../../../common/mock/match_media'; -import { DetectionEngineContainer } from './index'; - -describe('DetectionEngineContainer', () => { - it('renders correctly', () => { - const wrapper = shallow(<DetectionEngineContainer />); - - expect(wrapper.find('Switch')).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.tsx deleted file mode 100644 index 2a3d418f9c3d3..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/index.tsx +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { Route, Switch } from 'react-router-dom'; - -import { CreateRulePage } from './rules/create'; -import { DetectionEnginePage } from './detection_engine'; -import { EditRulePage } from './rules/edit'; -import { RuleDetailsPage } from './rules/details'; -import { RulesPage } from './rules'; - -const DetectionEngineContainerComponent: React.FC = () => ( - <Switch> - <Route path="/rules/id/:detailName/edit"> - <EditRulePage /> - </Route> - <Route path="/rules/id/:detailName"> - <RuleDetailsPage /> - </Route> - <Route path="/rules/create"> - <CreateRulePage /> - </Route> - <Route path="/rules"> - <RulesPage /> - </Route> - <Route exact path="" strict> - <DetectionEnginePage /> - </Route> - </Switch> -); - -export const DetectionEngineContainer = React.memo(DetectionEngineContainerComponent); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.tsx index 78fac10815d45..214a7ac24da8a 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/actions.tsx @@ -5,10 +5,12 @@ * 2.0. */ -import * as H from 'history'; import React, { Dispatch } from 'react'; +import { NavigateToAppOptions } from '../../../../../../../../../src/core/public'; +import { APP_ID } from '../../../../../../common/constants'; import { BulkAction } from '../../../../../../common/detection_engine/schemas/common/schemas'; import { CreateRulesSchema } from '../../../../../../common/detection_engine/schemas/request'; +import { SecurityPageName } from '../../../../../app/types'; import { getEditRuleUrl } from '../../../../../common/components/link_to/redirect_to_detection_engine'; import { ActionToaster, @@ -31,8 +33,14 @@ import { transformOutput } from '../../../../containers/detection_engine/rules/t import * as i18n from '../translations'; import { bucketRulesResponse, getExportedRulesCount } from './helpers'; -export const editRuleAction = (rule: Rule, history: H.History) => { - history.push(getEditRuleUrl(rule.id)); +export const editRuleAction = ( + ruleId: string, + navigateToApp: (appId: string, options?: NavigateToAppOptions | undefined) => Promise<void> +) => { + navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.rules, + path: getEditRuleUrl(ruleId ?? ''), + }); }; export const duplicateRulesAction = async ( diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.test.tsx index 8eb80bd0d5135..3920aa40e1f15 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.test.tsx @@ -39,26 +39,31 @@ describe('AllRulesTable Columns', () => { test('duplicate rule onClick should call rule edit after the rule is duplicated', async () => { const ruleDuplicate = mockRule('newRule'); + const navigateToApp = jest.fn(); duplicateRulesActionMock.mockImplementation(() => Promise.resolve([ruleDuplicate])); const duplicateRulesActionObject = getActions( dispatch, dispatchToaster, history, + navigateToApp, reFetchRules, refetchPrePackagedRulesStatus, true )[1]; await duplicateRulesActionObject.onClick(rule); expect(duplicateRulesActionMock).toHaveBeenCalled(); - expect(editRuleActionMock).toHaveBeenCalledWith(ruleDuplicate, history); + expect(editRuleActionMock).toHaveBeenCalledWith(ruleDuplicate.id, navigateToApp); }); test('delete rule onClick should call refetch after the rule is deleted', async () => { + const navigateToApp = jest.fn(); + const deleteRulesActionObject = getActions( dispatch, dispatchToaster, history, + navigateToApp, reFetchRules, refetchPrePackagedRulesStatus, true diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx index 28a65c3e64e1f..8d0492267258f 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx @@ -40,11 +40,14 @@ import { LinkAnchor } from '../../../../../common/components/links'; import { getToolTipContent, canEditRuleWithActions } from '../../../../../common/utils/privileges'; import { TagsDisplay } from './tag_display'; import { getRuleStatusText } from '../../../../../../common/detection_engine/utils'; +import { APP_ID, SecurityPageName } from '../../../../../../common/constants'; +import { NavigateToAppOptions } from '../../../../../../../../../src/core/public'; export const getActions = ( dispatch: React.Dispatch<RulesTableAction>, dispatchToaster: Dispatch<ActionToaster>, history: H.History, + navigateToApp: (appId: string, options?: NavigateToAppOptions | undefined) => Promise<void>, reFetchRules: () => Promise<void>, refetchPrePackagedRulesStatus: () => Promise<void>, actionsPrivileges: @@ -64,7 +67,7 @@ export const getActions = ( i18n.EDIT_RULE_SETTINGS ), icon: 'controlsHorizontal', - onClick: (rowItem: Rule) => editRuleAction(rowItem, history), + onClick: (rowItem: Rule) => editRuleAction(rowItem.id, navigateToApp), enabled: (rowItem: Rule) => canEditRuleWithActions(rowItem, actionsPrivileges), }, { @@ -87,7 +90,7 @@ export const getActions = ( dispatchToaster ); if (createdRules?.length) { - editRuleAction(createdRules[0], history); + editRuleAction(createdRules[0].id, navigateToApp); } }, }, @@ -127,6 +130,7 @@ interface GetColumns { hasMlPermissions: boolean; hasPermissions: boolean; loadingRuleIds: string[]; + navigateToApp: (appId: string, options?: NavigateToAppOptions | undefined) => Promise<void>; reFetchRules: () => Promise<void>; refetchPrePackagedRulesStatus: () => Promise<void>; hasReadActionsPrivileges: @@ -144,6 +148,7 @@ export const getColumns = ({ hasMlPermissions, hasPermissions, loadingRuleIds, + navigateToApp, reFetchRules, refetchPrePackagedRulesStatus, hasReadActionsPrivileges, @@ -157,7 +162,10 @@ export const getColumns = ({ data-test-subj="ruleName" onClick={(ev: { preventDefault: () => void }) => { ev.preventDefault(); - history.push(getRuleDetailsUrl(item.id)); + navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.rules, + path: getRuleDetailsUrl(item.id), + }); }} href={formatUrl(getRuleDetailsUrl(item.id))} > @@ -292,6 +300,7 @@ export const getColumns = ({ dispatch, dispatchToaster, history, + navigateToApp, reFetchRules, refetchPrePackagedRulesStatus, hasReadActionsPrivileges diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx index f64586db9b06c..c4c938d5bb05e 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/columns.tsx @@ -9,7 +9,6 @@ import React from 'react'; import { EuiButtonIcon, EuiBasicTableColumn, EuiToolTip } from '@elastic/eui'; -import { History } from 'history'; import type { NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { Spacer } from '../../../../../../common/components/page'; @@ -24,8 +23,8 @@ export type AllExceptionListsColumns = EuiBasicTableColumn<ExceptionListInfo>; export const getAllExceptionListsColumns = ( onExport: (arg: { id: string; listId: string; namespaceType: NamespaceType }) => () => void, onDelete: (arg: { id: string; listId: string; namespaceType: NamespaceType }) => () => void, - history: History, - formatUrl: FormatUrl + formatUrl: FormatUrl, + navigateToUrl: (url: string) => Promise<void> ): AllExceptionListsColumns[] => [ { align: 'left', @@ -81,7 +80,7 @@ export const getAllExceptionListsColumns = ( data-test-subj="ruleName" onClick={(ev: { preventDefault: () => void }) => { ev.preventDefault(); - history.push(getRuleDetailsUrl(id)); + navigateToUrl(formatUrl(getRuleDetailsUrl(id))); }} href={formatUrl(getRuleDetailsUrl(id))} > diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_search_bar.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_search_bar.tsx index 9c2b427948fd8..d86e7b1b7259c 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_search_bar.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_search_bar.tsx @@ -42,6 +42,7 @@ export const ExceptionsSearchBar = React.memo<ExceptionListsTableSearchProps>(({ aria-label={i18n.EXCEPTIONS_LISTS_SEARCH_PLACEHOLDER} onChange={onSearch} box={{ + [`data-test-subj`]: 'exceptionsHeaderSearchInput', placeholder: i18n.EXCEPTION_LIST_SEARCH_PLACEHOLDER, incremental: false, schema: EXCEPTIONS_SEARCH_SCHEMA, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx index 8cc3113a5706a..a3f1c8406b055 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.test.tsx @@ -13,13 +13,20 @@ import { mockHistory } from '../../../../../../common/utils/route/index.test'; import { getExceptionListSchemaMock } from '../../../../../../../../lists/common/schemas/response/exception_list_schema.mock'; import { ExceptionListsTable } from './exceptions_table'; -import { useKibana } from '../../../../../../common/lib/kibana'; import { useApi, useExceptionLists } from '@kbn/securitysolution-list-hooks'; import { useAllExceptionLists } from './use_all_exception_lists'; +import { useHistory } from 'react-router-dom'; jest.mock('../../../../../../common/lib/kibana'); jest.mock('./use_all_exception_lists'); jest.mock('@kbn/securitysolution-list-hooks'); +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); + return { + ...originalModule, + useHistory: jest.fn(), + }; +}); jest.mock('@kbn/i18n/react', () => { const originalModule = jest.requireActual('@kbn/i18n/react'); const FormattedRelative = jest.fn().mockImplementation(() => '20 hours ago'); @@ -29,25 +36,29 @@ jest.mock('@kbn/i18n/react', () => { FormattedRelative, }; }); + +jest.mock('../../../../../containers/detection_engine/lists/use_lists_config', () => ({ + useListsConfig: jest.fn().mockReturnValue({ loading: false }), +})); + +jest.mock('../../../../../components/user_info', () => ({ + useUserData: jest.fn().mockReturnValue([ + { + loading: false, + canUserCRUD: false, + }, + ]), +})); + describe('ExceptionListsTable', () => { const exceptionList1 = getExceptionListSchemaMock(); const exceptionList2 = { ...getExceptionListSchemaMock(), list_id: 'not_endpoint_list', id: '2' }; - beforeEach(() => { - (useKibana as jest.Mock).mockReturnValue({ - services: { - http: {}, - notifications: { - toasts: { - addError: jest.fn(), - }, - }, - timelines: { - getLastUpdated: () => null, - }, - }, - }); + beforeAll(() => { + (useHistory as jest.Mock).mockReturnValue(mockHistory); + }); + beforeEach(() => { (useApi as jest.Mock).mockReturnValue({ deleteExceptionList: jest.fn(), exportExceptionList: jest.fn(), @@ -80,15 +91,9 @@ describe('ExceptionListsTable', () => { it('renders delete option disabled if list is "endpoint_list"', async () => { const wrapper = mount( <TestProviders> - <ExceptionListsTable - history={mockHistory} - hasPermissions - loading={false} - formatUrl={jest.fn()} - /> + <ExceptionListsTable /> </TestProviders> ); - expect(wrapper.find('[data-test-subj="exceptionsTableListId"]').at(0).text()).toEqual( 'endpoint_list' ); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx index f38bde4839f18..b4f5efe2348bb 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx @@ -12,18 +12,19 @@ import { EuiLoadingContent, EuiProgress, EuiSearchBarProps, + EuiSpacer, } from '@elastic/eui'; -import { History } from 'history'; import type { NamespaceType, ExceptionListFilter } from '@kbn/securitysolution-io-ts-list-types'; import { useApi, useExceptionLists } from '@kbn/securitysolution-list-hooks'; import { useAppToasts } from '../../../../../../common/hooks/use_app_toasts'; import { AutoDownload } from '../../../../../../common/components/auto_download/auto_download'; import { useKibana } from '../../../../../../common/lib/kibana'; -import { FormatUrl } from '../../../../../../common/components/link_to'; -import { HeaderSection } from '../../../../../../common/components/header_section'; +import { useFormatUrl } from '../../../../../../common/components/link_to'; import { Loader } from '../../../../../../common/components/loader'; import { Panel } from '../../../../../../common/components/panel'; +import { DetectionEngineHeaderPage } from '../../../../../components/detection_engine_header_page'; + import * as i18n from './translations'; import { AllRulesUtilityBar } from '../utility_bar'; import { AllExceptionListsColumns, getAllExceptionListsColumns } from './columns'; @@ -32,16 +33,13 @@ import { ReferenceErrorModal } from '../../../../../components/value_lists_manag import { patchRule } from '../../../../../containers/detection_engine/rules/api'; import { ExceptionsSearchBar } from './exceptions_search_bar'; import { getSearchFilters } from '../helpers'; +import { SecurityPageName } from '../../../../../../../common/constants'; +import { useUserData } from '../../../../../components/user_info'; +import { userHasPermissions } from '../../helpers'; +import { useListsConfig } from '../../../../../containers/detection_engine/lists/use_lists_config'; export type Func = () => Promise<void>; -interface ExceptionListsTableProps { - history: History; - hasPermissions: boolean; - loading: boolean; - formatUrl: FormatUrl; -} - interface ReferenceModalState { contentText: string; rulesReferences: string[]; @@ -58,343 +56,346 @@ const exceptionReferenceModalInitialState: ReferenceModalState = { listNamespaceType: 'single', }; -export const ExceptionListsTable = React.memo<ExceptionListsTableProps>( - ({ formatUrl, history, hasPermissions, loading }) => { - const { - services: { http, notifications, timelines }, - } = useKibana(); - const { exportExceptionList, deleteExceptionList } = useApi(http); - - const [showReferenceErrorModal, setShowReferenceErrorModal] = useState(false); - const [referenceModalState, setReferenceModalState] = useState<ReferenceModalState>( - exceptionReferenceModalInitialState - ); - const [filters, setFilters] = useState<ExceptionListFilter | undefined>(undefined); - const [loadingExceptions, exceptions, pagination, refreshExceptions] = useExceptionLists({ - errorMessage: i18n.ERROR_EXCEPTION_LISTS, - filterOptions: filters, - http, - namespaceTypes: ['single', 'agnostic'], - notifications, - showTrustedApps: false, - showEventFilters: false, - }); - const [loadingTableInfo, exceptionListsWithRuleRefs, exceptionsListsRef] = useAllExceptionLists( - { - exceptionLists: exceptions ?? [], - } - ); - const [initLoading, setInitLoading] = useState(true); - const [lastUpdated, setLastUpdated] = useState(Date.now()); - const [deletingListIds, setDeletingListIds] = useState<string[]>([]); - const [exportingListIds, setExportingListIds] = useState<string[]>([]); - const [exportDownload, setExportDownload] = useState<{ name?: string; blob?: Blob }>({}); - const { addError } = useAppToasts(); - - const handleDeleteSuccess = useCallback( - (listId?: string) => () => { - notifications.toasts.addSuccess({ - title: i18n.exceptionDeleteSuccessMessage(listId ?? referenceModalState.listId), - }); - }, - [notifications.toasts, referenceModalState.listId] - ); +export const ExceptionListsTable = React.memo(() => { + const { formatUrl } = useFormatUrl(SecurityPageName.rules); + const [{ loading: userInfoLoading, canUserCRUD }] = useUserData(); + const hasPermissions = userHasPermissions(canUserCRUD); + + const { loading: listsConfigLoading } = useListsConfig(); + const loading = userInfoLoading || listsConfigLoading; + + const { + services: { http, notifications, timelines, application }, + } = useKibana(); + const { exportExceptionList, deleteExceptionList } = useApi(http); + + const [showReferenceErrorModal, setShowReferenceErrorModal] = useState(false); + const [referenceModalState, setReferenceModalState] = useState<ReferenceModalState>( + exceptionReferenceModalInitialState + ); + const [filters, setFilters] = useState<ExceptionListFilter | undefined>(undefined); + const [loadingExceptions, exceptions, pagination, refreshExceptions] = useExceptionLists({ + errorMessage: i18n.ERROR_EXCEPTION_LISTS, + filterOptions: filters, + http, + namespaceTypes: ['single', 'agnostic'], + notifications, + showTrustedApps: false, + showEventFilters: false, + }); + const [loadingTableInfo, exceptionListsWithRuleRefs, exceptionsListsRef] = useAllExceptionLists({ + exceptionLists: exceptions ?? [], + }); + const [initLoading, setInitLoading] = useState(true); + const [lastUpdated, setLastUpdated] = useState(Date.now()); + const [deletingListIds, setDeletingListIds] = useState<string[]>([]); + const [exportingListIds, setExportingListIds] = useState<string[]>([]); + const [exportDownload, setExportDownload] = useState<{ name?: string; blob?: Blob }>({}); + const { navigateToUrl } = application; + const { addError } = useAppToasts(); + + const handleDeleteSuccess = useCallback( + (listId?: string) => () => { + notifications.toasts.addSuccess({ + title: i18n.exceptionDeleteSuccessMessage(listId ?? referenceModalState.listId), + }); + }, + [notifications.toasts, referenceModalState.listId] + ); + + const handleDeleteError = useCallback( + (err: Error & { body?: { message: string } }): void => { + addError(err, { + title: i18n.EXCEPTION_DELETE_ERROR, + }); + }, + [addError] + ); + + const handleDelete = useCallback( + ({ + id, + listId, + namespaceType, + }: { + id: string; + listId: string; + namespaceType: NamespaceType; + }) => async () => { + try { + setDeletingListIds((ids) => [...ids, id]); + if (refreshExceptions != null) { + await refreshExceptions(); + } - const handleDeleteError = useCallback( - (err: Error & { body?: { message: string } }): void => { - addError(err, { - title: i18n.EXCEPTION_DELETE_ERROR, - }); - }, - [addError] - ); + if (exceptionsListsRef[id] != null && exceptionsListsRef[id].rules.length === 0) { + await deleteExceptionList({ + id, + namespaceType, + onError: handleDeleteError, + onSuccess: handleDeleteSuccess(listId), + }); - const handleDelete = useCallback( - ({ - id, - listId, - namespaceType, - }: { - id: string; - listId: string; - namespaceType: NamespaceType; - }) => async () => { - try { - setDeletingListIds((ids) => [...ids, id]); if (refreshExceptions != null) { - await refreshExceptions(); + refreshExceptions(); } - - if (exceptionsListsRef[id] != null && exceptionsListsRef[id].rules.length === 0) { - await deleteExceptionList({ - id, - namespaceType, - onError: handleDeleteError, - onSuccess: handleDeleteSuccess(listId), - }); - - if (refreshExceptions != null) { - refreshExceptions(); - } - } else { - setReferenceModalState({ - contentText: i18n.referenceErrorMessage(exceptionsListsRef[id].rules.length), - rulesReferences: exceptionsListsRef[id].rules.map(({ name }) => name), - isLoading: true, - listId: id, - listNamespaceType: namespaceType, - }); - setShowReferenceErrorModal(true); - } - // route to patch rules with associated exception list - } catch (error) { - handleDeleteError(error); - } finally { - setDeletingListIds((ids) => [...ids.filter((_id) => _id !== id)]); + } else { + setReferenceModalState({ + contentText: i18n.referenceErrorMessage(exceptionsListsRef[id].rules.length), + rulesReferences: exceptionsListsRef[id].rules.map(({ name }) => name), + isLoading: true, + listId: id, + listNamespaceType: namespaceType, + }); + setShowReferenceErrorModal(true); } - }, - [ - deleteExceptionList, - exceptionsListsRef, - handleDeleteError, - handleDeleteSuccess, - refreshExceptions, - ] - ); - - const handleExportSuccess = useCallback( - (listId: string) => (blob: Blob): void => { - setExportDownload({ name: listId, blob }); - }, - [] - ); - - const handleExportError = useCallback( - (err: Error) => { - addError(err, { title: i18n.EXCEPTION_EXPORT_ERROR }); - }, - [addError] - ); - - const handleExport = useCallback( - ({ + // route to patch rules with associated exception list + } catch (error) { + handleDeleteError(error); + } finally { + setDeletingListIds((ids) => [...ids.filter((_id) => _id !== id)]); + } + }, + [ + deleteExceptionList, + exceptionsListsRef, + handleDeleteError, + handleDeleteSuccess, + refreshExceptions, + ] + ); + + const handleExportSuccess = useCallback( + (listId: string) => (blob: Blob): void => { + setExportDownload({ name: listId, blob }); + }, + [] + ); + + const handleExportError = useCallback( + (err: Error) => { + addError(err, { title: i18n.EXCEPTION_EXPORT_ERROR }); + }, + [addError] + ); + + const handleExport = useCallback( + ({ + id, + listId, + namespaceType, + }: { + id: string; + listId: string; + namespaceType: NamespaceType; + }) => async () => { + setExportingListIds((ids) => [...ids, id]); + await exportExceptionList({ id, listId, namespaceType, - }: { - id: string; - listId: string; - namespaceType: NamespaceType; - }) => async () => { - setExportingListIds((ids) => [...ids, id]); - await exportExceptionList({ - id, - listId, - namespaceType, - onError: handleExportError, - onSuccess: handleExportSuccess(listId), - }); - }, - [exportExceptionList, handleExportError, handleExportSuccess] + onError: handleExportError, + onSuccess: handleExportSuccess(listId), + }); + }, + [exportExceptionList, handleExportError, handleExportSuccess] + ); + + const exceptionsColumns = useMemo((): AllExceptionListsColumns[] => { + return getAllExceptionListsColumns(handleExport, handleDelete, formatUrl, navigateToUrl); + }, [handleExport, handleDelete, formatUrl, navigateToUrl]); + + const handleRefresh = useCallback((): void => { + if (refreshExceptions != null) { + setLastUpdated(Date.now()); + refreshExceptions(); + } + }, [refreshExceptions]); + + useEffect(() => { + if (initLoading && !loading && !loadingExceptions && !loadingTableInfo) { + setInitLoading(false); + } + }, [initLoading, loading, loadingExceptions, loadingTableInfo]); + + const emptyPrompt = useMemo((): JSX.Element => { + return ( + <EuiEmptyPrompt + title={<h3>{i18n.NO_EXCEPTION_LISTS}</h3>} + titleSize="xs" + body={i18n.NO_LISTS_BODY} + /> ); - - const exceptionsColumns = useMemo((): AllExceptionListsColumns[] => { - return getAllExceptionListsColumns(handleExport, handleDelete, history, formatUrl); - }, [handleExport, handleDelete, history, formatUrl]); - - const handleRefresh = useCallback((): void => { - if (refreshExceptions != null) { - setLastUpdated(Date.now()); - refreshExceptions(); - } - }, [refreshExceptions]); - - useEffect(() => { - if (initLoading && !loading && !loadingExceptions && !loadingTableInfo) { - setInitLoading(false); - } - }, [initLoading, loading, loadingExceptions, loadingTableInfo]); - - const emptyPrompt = useMemo((): JSX.Element => { - return ( - <EuiEmptyPrompt - title={<h3>{i18n.NO_EXCEPTION_LISTS}</h3>} - titleSize="xs" - body={i18n.NO_LISTS_BODY} - /> - ); - }, []); - - const handleSearch = useCallback( - async ({ + }, []); + + const handleSearch = useCallback( + async ({ + query, + queryText, + }: Parameters<NonNullable<EuiSearchBarProps['onChange']>>[0]): Promise<void> => { + const filterOptions = { + name: null, + list_id: null, + created_by: null, + type: null, + tags: null, + }; + const searchTerms = getSearchFilters({ + defaultSearchTerm: 'name', + filterOptions, query, - queryText, - }: Parameters<NonNullable<EuiSearchBarProps['onChange']>>[0]): Promise<void> => { - const filterOptions = { - name: null, - list_id: null, - created_by: null, - type: null, - tags: null, - }; - const searchTerms = getSearchFilters({ - defaultSearchTerm: 'name', - filterOptions, - query, - searchValue: queryText, - }); - setFilters(searchTerms); - }, - [] - ); + searchValue: queryText, + }); + setFilters(searchTerms); + }, + [] + ); + + const handleCloseReferenceErrorModal = useCallback((): void => { + setDeletingListIds([]); + setShowReferenceErrorModal(false); + setReferenceModalState({ + contentText: '', + rulesReferences: [], + isLoading: false, + listId: '', + listNamespaceType: 'single', + }); + }, []); + + const handleReferenceDelete = useCallback(async (): Promise<void> => { + const exceptionListId = referenceModalState.listId; + const exceptionListNamespaceType = referenceModalState.listNamespaceType; + const relevantRules = exceptionsListsRef[exceptionListId].rules; + + try { + await Promise.all( + relevantRules.map((rule) => { + const abortCtrl = new AbortController(); + const exceptionLists = (rule.exceptions_list ?? []).filter( + ({ id }) => id !== exceptionListId + ); + + return patchRule({ + ruleProperties: { + rule_id: rule.rule_id, + exceptions_list: exceptionLists, + }, + signal: abortCtrl.signal, + }); + }) + ); - const handleCloseReferenceErrorModal = useCallback((): void => { + await deleteExceptionList({ + id: exceptionListId, + namespaceType: exceptionListNamespaceType, + onError: handleDeleteError, + onSuccess: handleDeleteSuccess(), + }); + } catch (err) { + handleDeleteError(err); + } finally { + setReferenceModalState(exceptionReferenceModalInitialState); setDeletingListIds([]); setShowReferenceErrorModal(false); - setReferenceModalState({ - contentText: '', - rulesReferences: [], - isLoading: false, - listId: '', - listNamespaceType: 'single', - }); - }, []); - - const handleReferenceDelete = useCallback(async (): Promise<void> => { - const exceptionListId = referenceModalState.listId; - const exceptionListNamespaceType = referenceModalState.listNamespaceType; - const relevantRules = exceptionsListsRef[exceptionListId].rules; - - try { - await Promise.all( - relevantRules.map((rule) => { - const abortCtrl = new AbortController(); - const exceptionLists = (rule.exceptions_list ?? []).filter( - ({ id }) => id !== exceptionListId - ); - - return patchRule({ - ruleProperties: { - rule_id: rule.rule_id, - exceptions_list: exceptionLists, - }, - signal: abortCtrl.signal, - }); - }) - ); - - await deleteExceptionList({ - id: exceptionListId, - namespaceType: exceptionListNamespaceType, - onError: handleDeleteError, - onSuccess: handleDeleteSuccess(), - }); - } catch (err) { - handleDeleteError(err); - } finally { - setReferenceModalState(exceptionReferenceModalInitialState); - setDeletingListIds([]); - setShowReferenceErrorModal(false); - if (refreshExceptions != null) { - refreshExceptions(); - } + if (refreshExceptions != null) { + refreshExceptions(); } - }, [ - referenceModalState.listId, - referenceModalState.listNamespaceType, - exceptionsListsRef, - deleteExceptionList, - handleDeleteError, - handleDeleteSuccess, - refreshExceptions, - ]); - - const paginationMemo = useMemo( - () => ({ - pageIndex: pagination.page - 1, - pageSize: pagination.perPage, - totalItemCount: pagination.total || 0, - pageSizeOptions: [5, 10, 20, 50, 100, 200, 300], - }), - [pagination] - ); - - const handleOnDownload = useCallback(() => { - setExportDownload({}); - }, []); - - const tableItems = (exceptionListsWithRuleRefs ?? []).map((item) => ({ - ...item, - isDeleting: deletingListIds.includes(item.id), - isExporting: exportingListIds.includes(item.id), - })); - - return ( - <> - <Panel loading={!initLoading && loadingTableInfo} data-test-subj="allExceptionListsPanel"> - <> - {loadingTableInfo && ( - <EuiProgress - data-test-subj="loadingRulesInfoProgress" - size="xs" - position="absolute" - color="accent" + } + }, [ + referenceModalState.listId, + referenceModalState.listNamespaceType, + exceptionsListsRef, + deleteExceptionList, + handleDeleteError, + handleDeleteSuccess, + refreshExceptions, + ]); + + const paginationMemo = useMemo( + () => ({ + pageIndex: pagination.page - 1, + pageSize: pagination.perPage, + totalItemCount: pagination.total || 0, + pageSizeOptions: [5, 10, 20, 50, 100, 200, 300], + }), + [pagination] + ); + + const handleOnDownload = useCallback(() => { + setExportDownload({}); + }, []); + + const tableItems = (exceptionListsWithRuleRefs ?? []).map((item) => ({ + ...item, + isDeleting: deletingListIds.includes(item.id), + isExporting: exportingListIds.includes(item.id), + })); + + return ( + <> + <DetectionEngineHeaderPage + title={i18n.ALL_EXCEPTIONS} + subtitle={timelines.getLastUpdated({ showUpdating: loading, updatedAt: lastUpdated })} + /> + <Panel loading={!initLoading && loadingTableInfo} data-test-subj="allExceptionListsPanel"> + <> + {loadingTableInfo && ( + <EuiProgress + data-test-subj="loadingRulesInfoProgress" + size="xs" + position="absolute" + color="accent" + /> + )} + {!initLoading && <ExceptionsSearchBar onSearch={handleSearch} />} + <EuiSpacer size="m" /> + + {loadingTableInfo && !initLoading && !showReferenceErrorModal && ( + <Loader data-test-subj="loadingPanelAllRulesTable" overlay size="xl" /> + )} + + {initLoading ? ( + <EuiLoadingContent data-test-subj="initialLoadingPanelAllRulesTable" lines={10} /> + ) : ( + <> + <AllRulesUtilityBar + showBulkActions={false} + canBulkEdit={hasPermissions} + paginationTotal={exceptionListsWithRuleRefs.length ?? 0} + numberSelectedItems={0} + onRefresh={handleRefresh} /> - )} - <HeaderSection - split - title={i18n.ALL_EXCEPTIONS} - subtitle={timelines.getLastUpdated({ showUpdating: loading, updatedAt: lastUpdated })} - > - {!initLoading && <ExceptionsSearchBar onSearch={handleSearch} />} - </HeaderSection> - - {loadingTableInfo && !initLoading && !showReferenceErrorModal && ( - <Loader data-test-subj="loadingPanelAllRulesTable" overlay size="xl" /> - )} - - {initLoading ? ( - <EuiLoadingContent data-test-subj="initialLoadingPanelAllRulesTable" lines={10} /> - ) : ( - <> - <AllRulesUtilityBar - showBulkActions={false} - canBulkEdit={hasPermissions} - paginationTotal={exceptionListsWithRuleRefs.length ?? 0} - numberSelectedItems={0} - onRefresh={handleRefresh} - /> - <EuiBasicTable - data-test-subj="exceptions-table" - columns={exceptionsColumns} - isSelectable={hasPermissions} - itemId="id" - items={tableItems} - noItemsMessage={emptyPrompt} - onChange={() => {}} - pagination={paginationMemo} - /> - </> - )} - </> - </Panel> - <AutoDownload - blob={exportDownload.blob} - name={`${exportDownload.name}.ndjson`} - onDownload={handleOnDownload} - /> - <ReferenceErrorModal - cancelText={i18n.REFERENCE_MODAL_CANCEL_BUTTON} - confirmText={i18n.REFERENCE_MODAL_CONFIRM_BUTTON} - contentText={referenceModalState.contentText} - onCancel={handleCloseReferenceErrorModal} - onClose={handleCloseReferenceErrorModal} - onConfirm={handleReferenceDelete} - references={referenceModalState.rulesReferences} - showModal={showReferenceErrorModal} - titleText={i18n.REFERENCE_MODAL_TITLE} - /> - </> - ); - } -); + <EuiBasicTable + data-test-subj="exceptions-table" + columns={exceptionsColumns} + isSelectable={hasPermissions} + itemId="id" + items={tableItems} + noItemsMessage={emptyPrompt} + onChange={() => {}} + pagination={paginationMemo} + /> + </> + )} + </> + </Panel> + <AutoDownload + blob={exportDownload.blob} + name={`${exportDownload.name}.ndjson`} + onDownload={handleOnDownload} + /> + <ReferenceErrorModal + cancelText={i18n.REFERENCE_MODAL_CANCEL_BUTTON} + confirmText={i18n.REFERENCE_MODAL_CONFIRM_BUTTON} + contentText={referenceModalState.contentText} + onCancel={handleCloseReferenceErrorModal} + onClose={handleCloseReferenceErrorModal} + onConfirm={handleReferenceDelete} + references={referenceModalState.rulesReferences} + showModal={showReferenceErrorModal} + titleText={i18n.REFERENCE_MODAL_TITLE} + /> + </> + ); +}); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx index 9597c221843be..200bf0c719320 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx @@ -326,33 +326,4 @@ describe('AllRules', () => { expect(wrapper.exists('[data-test-subj="rules-table"]')).toBeFalsy(); }); }); - - it('renders exceptions lists tab when tab clicked', async () => { - const wrapper = mount( - <TestProviders> - <AllRules - createPrePackagedRules={jest.fn()} - hasPermissions - loading={false} - loadingCreatePrePackagedRules={false} - refetchPrePackagedRulesStatus={jest.fn()} - rulesCustomInstalled={1} - rulesInstalled={0} - rulesNotInstalled={0} - rulesNotUpdated={0} - setRefreshRulesData={jest.fn()} - /> - </TestProviders> - ); - - await waitFor(() => { - const exceptionsTab = wrapper.find('[data-test-subj="allRulesTableTab-exceptions"] button'); - exceptionsTab.simulate('click'); - - wrapper.update(); - expect(wrapper.exists('[data-test-subj="allExceptionListsPanel"]')).toBeTruthy(); - expect(wrapper.exists('[data-test-subj="rules-table"]')).toBeFalsy(); - expect(wrapper.exists('[data-test-subj="monitoring-table"]')).toBeFalsy(); - }); - }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx index 144aa05cb3b07..76a049936a722 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx @@ -14,7 +14,6 @@ import { useFormatUrl } from '../../../../../common/components/link_to'; import { CreatePreBuiltRules } from '../../../../containers/detection_engine/rules'; import { RulesTables } from './rules_tables'; import * as i18n from '../translations'; -import { ExceptionListsTable } from './exceptions/exceptions_table'; interface AllRulesProps { createPrePackagedRules: CreatePreBuiltRules | null; @@ -46,13 +45,7 @@ const allRulesTabs = [ name: i18n.MONITORING_TAB, disabled: false, }, - { - id: AllRulesTabs.exceptions, - name: i18n.EXCEPTIONS_TAB, - disabled: false, - }, ]; - /** * Table Component for displaying all Rules for a given cluster. Provides the ability to filter * by name, sort by enabled, and perform the following actions: @@ -75,7 +68,7 @@ export const AllRules = React.memo<AllRulesProps>( setRefreshRulesData, }) => { const history = useHistory(); - const { formatUrl } = useFormatUrl(SecurityPageName.detections); + const { formatUrl } = useFormatUrl(SecurityPageName.rules); const [allRulesTab, setAllRulesTab] = useState(AllRulesTabs.rules); const tabs = useMemo( @@ -100,35 +93,23 @@ export const AllRules = React.memo<AllRulesProps>( return ( <> - <EuiSpacer /> {tabs} <EuiSpacer /> - - {(allRulesTab === AllRulesTabs.rules || allRulesTab === AllRulesTabs.monitoring) && ( - <RulesTables - history={history} - formatUrl={formatUrl} - selectedTab={allRulesTab} - createPrePackagedRules={createPrePackagedRules} - hasPermissions={hasPermissions} - loading={loading} - loadingCreatePrePackagedRules={loadingCreatePrePackagedRules} - refetchPrePackagedRulesStatus={refetchPrePackagedRulesStatus} - rulesCustomInstalled={rulesCustomInstalled} - rulesInstalled={rulesInstalled} - rulesNotInstalled={rulesNotInstalled} - rulesNotUpdated={rulesNotUpdated} - setRefreshRulesData={setRefreshRulesData} - /> - )} - {allRulesTab === AllRulesTabs.exceptions && ( - <ExceptionListsTable - formatUrl={formatUrl} - history={history} - hasPermissions={hasPermissions} - loading={loading} - /> - )} + <RulesTables + history={history} + formatUrl={formatUrl} + selectedTab={allRulesTab} + createPrePackagedRules={createPrePackagedRules} + hasPermissions={hasPermissions} + loading={loading} + loadingCreatePrePackagedRules={loadingCreatePrePackagedRules} + refetchPrePackagedRulesStatus={refetchPrePackagedRulesStatus} + rulesCustomInstalled={rulesCustomInstalled} + rulesInstalled={rulesInstalled} + rulesNotInstalled={rulesNotInstalled} + rulesNotUpdated={rulesNotUpdated} + setRefreshRulesData={setRefreshRulesData} + /> </> ); } diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx index 2ec34aaece60b..77ca5be0c0ac1 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx @@ -148,6 +148,7 @@ export const RulesTables = React.memo<RulesTableProps>( const { loading: isLoadingRulesStatuses, rulesStatuses } = useRulesStatuses(rules); const [, dispatchToaster] = useStateToaster(); const mlCapabilities = useMlCapabilities(); + const { navigateToApp } = useKibana().services.application; // TODO: Refactor license check + hasMlAdminPermissions to common check const hasMlPermissions = hasMlLicense(mlCapabilities) && hasMlAdminPermissions(mlCapabilities); @@ -279,6 +280,7 @@ export const RulesTables = React.memo<RulesTableProps>( (loadingRulesAction === 'enable' || loadingRulesAction === 'disable') ? loadingRuleIds : [], + navigateToApp, reFetchRules, refetchPrePackagedRulesStatus, hasReadActionsPrivileges: hasActionsPrivileges, @@ -294,6 +296,7 @@ export const RulesTables = React.memo<RulesTableProps>( history, loadingRuleIds, loadingRulesAction, + navigateToApp, reFetchRules, ]); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.test.tsx index 9622610f3c637..45713b6b0667f 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.test.tsx @@ -25,7 +25,7 @@ jest.mock('react-router-dom', () => { }), }; }); - +jest.mock('../../../../../common/lib/kibana'); jest.mock('../../../../containers/detection_engine/lists/use_lists_config'); jest.mock('../../../../../common/components/link_to'); jest.mock('../../../../components/user_info'); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx index 23edf785a7f3a..3d488f1f08c98 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx @@ -14,7 +14,6 @@ import { EuiFlexGroup, } from '@elastic/eui'; import React, { useCallback, useRef, useState, useMemo } from 'react'; -import { useHistory } from 'react-router-dom'; import styled, { StyledComponent } from 'styled-components'; import { useCreateRule } from '../../../../containers/detection_engine/rules'; @@ -48,6 +47,8 @@ import { formatRule, stepIsValid } from './helpers'; import * as i18n from './translations'; import { SecurityPageName } from '../../../../../app/types'; import { ruleStepsOrder } from '../utils'; +import { APP_ID } from '../../../../../../common/constants'; +import { useKibana } from '../../../../../common/lib/kibana'; const formHookNoop = async (): Promise<undefined> => undefined; @@ -100,6 +101,7 @@ const CreateRulePageComponent: React.FC = () => { loading: listsConfigLoading, needsConfiguration: needsListsConfiguration, } = useListsConfig(); + const { navigateToApp } = useKibana().services.application; const loading = userInfoLoading || listsConfigLoading; const [, dispatchToaster] = useStateToaster(); const [activeStep, setActiveStep] = useState<RuleStep>(RuleStep.defineRule); @@ -143,7 +145,6 @@ const CreateRulePageComponent: React.FC = () => { const ruleType = stepsData.current[RuleStep.defineRule].data?.ruleType; const ruleName = stepsData.current[RuleStep.aboutRule].data?.name; const actionMessageParams = useMemo(() => getActionMessageParams(ruleType), [ruleType]); - const history = useHistory(); const handleAccordionToggle = useCallback( (step: RuleStep, isOpen: boolean) => @@ -235,6 +236,10 @@ const CreateRulePageComponent: React.FC = () => { [activeStep] ); + const submitStepDefineRule = useCallback(() => { + submitStep(RuleStep.defineRule); + }, [submitStep]); + const defineRuleButton = ( <AccordionTitle name="1" @@ -266,7 +271,10 @@ const CreateRulePageComponent: React.FC = () => { if (ruleName && ruleId) { displaySuccessToast(i18n.SUCCESSFULLY_CREATED_RULES(ruleName), dispatchToaster); - history.replace(getRuleDetailsUrl(ruleId)); + navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.rules, + path: getRuleDetailsUrl(ruleId), + }); return null; } @@ -278,13 +286,18 @@ const CreateRulePageComponent: React.FC = () => { needsListsConfiguration ) ) { - history.replace(getDetectionEngineUrl()); + navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.alerts, + path: getDetectionEngineUrl(), + }); return null; } else if (!userHasPermissions(canUserCRUD)) { - history.replace(getRulesUrl()); + navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.rules, + path: getRulesUrl(), + }); return null; } - return ( <> <SecuritySolutionPageWrapper> @@ -294,7 +307,7 @@ const CreateRulePageComponent: React.FC = () => { backOptions={{ href: getRulesUrl(), text: i18n.BACK_TO_RULES, - pageId: SecurityPageName.detections, + pageId: SecurityPageName.rules, }} isLoading={isLoading || loading} title={i18n.PAGE_TITLE} @@ -327,7 +340,7 @@ const CreateRulePageComponent: React.FC = () => { isReadOnlyView={activeStep !== RuleStep.defineRule} isLoading={isLoading || loading} setForm={setFormHook} - onSubmit={() => submitStep(RuleStep.defineRule)} + onSubmit={submitStepDefineRule} descriptionColumns="singleSplit" /> </StepDefineRuleAccordion> @@ -437,7 +450,7 @@ const CreateRulePageComponent: React.FC = () => { </EuiFlexGroup> </SecuritySolutionPageWrapper> - <SpyRoute pageName={SecurityPageName.detections} /> + <SpyRoute pageName={SecurityPageName.rules} /> </> ); }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 92679cb2662d7..a7a9b31d1f408 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -22,7 +22,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { noop } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { useParams, useHistory } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; @@ -86,7 +86,7 @@ import { SecurityPageName } from '../../../../../app/types'; import { LinkButton } from '../../../../../common/components/links'; import { useFormatUrl } from '../../../../../common/components/link_to'; import { ExceptionsViewer } from '../../../../../common/components/exceptions/viewer'; -import { DEFAULT_INDEX_PATTERN } from '../../../../../../common/constants'; +import { APP_ID, DEFAULT_INDEX_PATTERN } from '../../../../../../common/constants'; import { useGlobalFullScreen } from '../../../../../common/containers/use_full_screen'; import { Display } from '../../../../../hosts/pages/display'; @@ -153,6 +153,7 @@ const ruleDetailTabs = [ ]; const RuleDetailsPageComponent = () => { + const { navigateToApp } = useKibana().services.application; const dispatch = useDispatch(); const containerElement = useRef<HTMLDivElement | null>(null); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); @@ -220,8 +221,7 @@ const RuleDetailsPageComponent = () => { const [showBuildingBlockAlerts, setShowBuildingBlockAlerts] = useState(false); const [showOnlyThreatIndicatorAlerts, setShowOnlyThreatIndicatorAlerts] = useState(false); const mlCapabilities = useMlCapabilities(); - const history = useHistory(); - const { formatUrl } = useFormatUrl(SecurityPageName.detections); + const { formatUrl } = useFormatUrl(SecurityPageName.rules); const { globalFullScreen } = useGlobalFullScreen(); // TODO: Once we are past experimental phase this code should be removed @@ -442,9 +442,12 @@ const RuleDetailsPageComponent = () => { const goToEditRule = useCallback( (ev) => { ev.preventDefault(); - history.push(getEditRuleUrl(ruleId ?? '')); + navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.rules, + path: getEditRuleUrl(ruleId ?? ''), + }); }, - [history, ruleId] + [navigateToApp, ruleId] ); const editRule = useMemo(() => { @@ -548,7 +551,10 @@ const RuleDetailsPageComponent = () => { needsListsConfiguration ) ) { - history.replace(getDetectionEngineUrl()); + navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.alerts, + path: getDetectionEngineUrl(), + }); return null; } @@ -574,7 +580,7 @@ const RuleDetailsPageComponent = () => { backOptions={{ href: getRulesUrl(), text: i18n.BACK_TO_RULES, - pageId: SecurityPageName.detections, + pageId: SecurityPageName.rules, dataTestSubj: 'ruleDetailsBackToAllRules', }} border @@ -744,7 +750,7 @@ const RuleDetailsPageComponent = () => { </SecuritySolutionPageWrapper> )} - <SpyRoute pageName={SecurityPageName.detections} state={{ ruleName: rule?.name }} /> + <SpyRoute pageName={SecurityPageName.rules} state={{ ruleName: rule?.name }} /> </> ); }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.test.tsx index e7cdfbe268fe6..e56742b9f0a51 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.test.tsx @@ -16,6 +16,7 @@ import { useParams } from 'react-router-dom'; import { useAppToastsMock } from '../../../../../common/hooks/use_app_toasts.mock'; import { useAppToasts } from '../../../../../common/hooks/use_app_toasts'; +jest.mock('../../../../../common/lib/kibana'); jest.mock('../../../../containers/detection_engine/lists/use_lists_config'); jest.mock('../../../../../common/components/link_to'); jest.mock('../../../../components/user_info'); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx index 41710a822e539..4786d7f2eae78 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx @@ -16,7 +16,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { FC, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { useParams, useHistory } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; import { UpdateRulesSchema } from '../../../../../../common/detection_engine/schemas/request'; import { useRule, useUpdateRule } from '../../../../containers/detection_engine/rules'; @@ -55,11 +55,12 @@ import { RuleStep, RuleStepsFormHooks, RuleStepsFormData, RuleStepsData } from ' import * as i18n from './translations'; import { SecurityPageName } from '../../../../../app/types'; import { ruleStepsOrder } from '../utils'; +import { useKibana } from '../../../../../common/lib/kibana'; +import { APP_ID } from '../../../../../../common/constants'; const formHookNoop = async (): Promise<undefined> => undefined; const EditRulePageComponent: FC = () => { - const history = useHistory(); const [, dispatchToaster] = useStateToaster(); const [ { @@ -74,6 +75,8 @@ const EditRulePageComponent: FC = () => { loading: listsConfigLoading, needsConfiguration: needsListsConfiguration, } = useListsConfig(); + const { navigateToApp } = useKibana().services.application; + const { detailName: ruleId } = useParams<{ detailName: string | undefined }>(); const [ruleLoading, rule] = useRule(ruleId); const loading = ruleLoading || userInfoLoading || listsConfigLoading; @@ -299,9 +302,12 @@ const EditRulePageComponent: FC = () => { const goToDetailsRule = useCallback( (ev) => { ev.preventDefault(); - history.replace(getRuleDetailsUrl(ruleId ?? '')); + navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.rules, + path: getRuleDetailsUrl(ruleId ?? ''), + }); }, - [history, ruleId] + [navigateToApp, ruleId] ); useEffect(() => { @@ -314,7 +320,10 @@ const EditRulePageComponent: FC = () => { if (isSaved) { displaySuccessToast(i18n.SUCCESSFULLY_SAVED_RULE(rule?.name ?? ''), dispatchToaster); - history.replace(getRuleDetailsUrl(ruleId ?? '')); + navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.rules, + path: getRuleDetailsUrl(ruleId ?? ''), + }); return null; } @@ -326,10 +335,16 @@ const EditRulePageComponent: FC = () => { needsListsConfiguration ) ) { - history.replace(getDetectionEngineUrl()); + navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.alerts, + path: getDetectionEngineUrl(), + }); return null; } else if (!userHasPermissions(canUserCRUD)) { - history.replace(getRuleDetailsUrl(ruleId ?? '')); + navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.rules, + path: getRuleDetailsUrl(ruleId ?? ''), + }); return null; } @@ -342,7 +357,7 @@ const EditRulePageComponent: FC = () => { backOptions={{ href: getRuleDetailsUrl(ruleId ?? ''), text: `${i18n.BACK_TO} ${rule?.name ?? ''}`, - pageId: SecurityPageName.detections, + pageId: SecurityPageName.rules, dataTestSubj: 'ruleEditBackToRuleDetails', }} isLoading={isLoading} @@ -412,7 +427,7 @@ const EditRulePageComponent: FC = () => { </EuiFlexGroup> </SecuritySolutionPageWrapper> - <SpyRoute pageName={SecurityPageName.detections} state={{ ruleName: rule?.name }} /> + <SpyRoute pageName={SecurityPageName.rules} state={{ ruleName: rule?.name }} /> </> ); }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx index bcd5ccdc0b5ac..550a289a11a97 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.test.tsx @@ -30,6 +30,8 @@ jest.mock('react-router-dom', () => { jest.mock('../../../containers/detection_engine/lists/use_lists_config'); jest.mock('../../../../common/components/link_to'); jest.mock('../../../components/user_info'); + +jest.mock('../../../../common/lib/kibana'); jest.mock('../../../../common/components/toasters', () => { const actual = jest.requireActual('../../../../common/components/toasters'); return { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx index 29fd8e2e8b247..f957f77ac4c1a 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/index.tsx @@ -7,7 +7,6 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import React, { useCallback, useMemo, useRef, useState } from 'react'; -import { useHistory } from 'react-router-dom'; import { usePrePackagedRules, importRules } from '../../../containers/detection_engine/rules'; import { useListsConfig } from '../../../containers/detection_engine/lists/use_lists_config'; @@ -37,14 +36,17 @@ import { useFormatUrl } from '../../../../common/components/link_to'; import { NeedAdminForUpdateRulesCallOut } from '../../../components/callouts/need_admin_for_update_callout'; import { MlJobCompatibilityCallout } from '../../../components/callouts/ml_job_compatibility_callout'; import { MissingPrivilegesCallOut } from '../../../components/callouts/missing_privileges_callout'; +import { APP_ID } from '../../../../../common/constants'; +import { useKibana } from '../../../../common/lib/kibana'; type Func = () => Promise<void>; const RulesPageComponent: React.FC = () => { - const history = useHistory(); const [showImportModal, setShowImportModal] = useState(false); const [showValueListsModal, setShowValueListsModal] = useState(false); const refreshRulesData = useRef<null | Func>(null); + const { navigateToApp } = useKibana().services.application; + const [ { loading: userInfoLoading, @@ -93,7 +95,7 @@ const RulesPageComponent: React.FC = () => { timelinesNotInstalled, timelinesNotUpdated ); - const { formatUrl } = useFormatUrl(SecurityPageName.detections); + const { formatUrl } = useFormatUrl(SecurityPageName.rules); const handleRefreshRules = useCallback(async () => { if (refreshRulesData.current != null) { @@ -123,9 +125,9 @@ const RulesPageComponent: React.FC = () => { const goToNewRule = useCallback( (ev) => { ev.preventDefault(); - history.push(getCreateRuleUrl()); + navigateToApp(APP_ID, { deepLinkId: SecurityPageName.rules, path: getCreateRuleUrl() }); }, - [history] + [navigateToApp] ); const loadPrebuiltRulesAndTemplatesButton = useMemo( @@ -154,7 +156,10 @@ const RulesPageComponent: React.FC = () => { needsListsConfiguration ) ) { - history.replace(getDetectionEngineUrl()); + navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.alerts, + path: getDetectionEngineUrl(), + }); return null; } @@ -183,14 +188,7 @@ const RulesPageComponent: React.FC = () => { title={i18n.IMPORT_RULE} /> <SecuritySolutionPageWrapper> - <DetectionEngineHeaderPage - backOptions={{ - href: getDetectionEngineUrl(), - text: i18n.BACK_TO_DETECTIONS, - pageId: SecurityPageName.detections, - }} - title={i18n.PAGE_TITLE} - > + <DetectionEngineHeaderPage title={i18n.PAGE_TITLE}> <EuiFlexGroup alignItems="center" gutterSize="s" responsive={false} wrap={true}> {loadPrebuiltRulesAndTemplatesButton && ( <EuiFlexItem grow={false}>{loadPrebuiltRulesAndTemplatesButton}</EuiFlexItem> @@ -260,7 +258,7 @@ const RulesPageComponent: React.FC = () => { /> </SecuritySolutionPageWrapper> - <SpyRoute pageName={SecurityPageName.detections} /> + <SpyRoute pageName={SecurityPageName.rules} /> </> ); }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts index defd976a04c4b..8379bbcb590e1 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts @@ -44,7 +44,7 @@ export const ADD_NEW_RULE = i18n.translate( ); export const PAGE_TITLE = i18n.translate('xpack.securitySolution.detectionEngine.rules.pageTitle', { - defaultMessage: 'Detection rules', + defaultMessage: 'Rules', }); export const ADD_PAGE_TITLE = i18n.translate( diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.test.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.test.ts index 217f786cb0a6a..d405837a4f7f2 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.test.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.test.ts @@ -11,7 +11,7 @@ const getUrlForAppMock = (appId: string, options?: { path?: string; absolute?: b `${appId}${options?.path ?? ''}`; describe('getBreadcrumbs', () => { - it('returns default value for incorrect params', () => { + it('Does not render for incorrect params', () => { expect( getBreadcrumbs( { @@ -24,6 +24,6 @@ describe('getBreadcrumbs', () => { [], getUrlForAppMock ) - ).toEqual([{ href: 'securitySolution:detections', text: 'Detections' }]); + ).toEqual([]); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts index 421ca6209e0af..bbc085eaa0be8 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/utils.ts @@ -9,18 +9,16 @@ import { isEmpty } from 'lodash/fp'; import { ChromeBreadcrumb } from '../../../../../../../../src/core/public'; import { - getDetectionEngineTabUrl, getRulesUrl, getRuleDetailsUrl, getCreateRuleUrl, getEditRuleUrl, } from '../../../../common/components/link_to/redirect_to_detection_engine'; -import * as i18nDetections from '../translations'; import * as i18nRules from './translations'; import { RouteSpyState } from '../../../../common/utils/route/types'; import { GetUrlForApp } from '../../../../common/components/navigation/types'; import { SecurityPageName } from '../../../../app/types'; -import { APP_ID } from '../../../../../common/constants'; +import { APP_ID, RULES_PATH } from '../../../../../common/constants'; import { RuleStep, RuleStepsOrder } from './types'; export const ruleStepsOrder: RuleStepsOrder = [ @@ -30,22 +28,14 @@ export const ruleStepsOrder: RuleStepsOrder = [ RuleStep.ruleActions, ]; -const getTabBreadcrumb = (pathname: string, search: string[], getUrlForApp: GetUrlForApp) => { +const getRulesBreadcrumb = (pathname: string, search: string[], getUrlForApp: GetUrlForApp) => { const tabPath = pathname.split('/')[1]; - if (tabPath === 'alerts') { - return { - text: i18nDetections.ALERT, - href: getUrlForApp(`${APP_ID}:${SecurityPageName.detections}`, { - path: getDetectionEngineTabUrl(tabPath, !isEmpty(search[0]) ? search[0] : ''), - }), - }; - } - if (tabPath === 'rules') { return { text: i18nRules.PAGE_TITLE, - href: getUrlForApp(`${APP_ID}:${SecurityPageName.detections}`, { + href: getUrlForApp(APP_ID, { + deepLinkId: SecurityPageName.rules, path: getRulesUrl(!isEmpty(search[0]) ? search[0] : ''), }), }; @@ -53,29 +43,22 @@ const getTabBreadcrumb = (pathname: string, search: string[], getUrlForApp: GetU }; const isRuleCreatePage = (pathname: string) => - pathname.includes('/rules') && pathname.includes('/create'); + pathname.includes(RULES_PATH) && pathname.includes('/create'); const isRuleEditPage = (pathname: string) => - pathname.includes('/rules') && pathname.includes('/edit'); + pathname.includes(RULES_PATH) && pathname.includes('/edit'); export const getBreadcrumbs = ( params: RouteSpyState, search: string[], getUrlForApp: GetUrlForApp ): ChromeBreadcrumb[] => { - let breadcrumb = [ - { - text: i18nDetections.BREADCRUMB_TITLE, - href: getUrlForApp(`${APP_ID}:${SecurityPageName.detections}`, { - path: !isEmpty(search[0]) ? search[0] : '', - }), - }, - ]; + let breadcrumb: ChromeBreadcrumb[] = []; - const tabBreadcrumb = getTabBreadcrumb(params.pathName, search, getUrlForApp); + const rulesBreadcrumb = getRulesBreadcrumb(params.pathName, search, getUrlForApp); - if (tabBreadcrumb) { - breadcrumb = [...breadcrumb, tabBreadcrumb]; + if (rulesBreadcrumb) { + breadcrumb = [...breadcrumb, rulesBreadcrumb]; } if (params.detailName && params.state?.ruleName) { @@ -83,7 +66,8 @@ export const getBreadcrumbs = ( ...breadcrumb, { text: params.state.ruleName, - href: getUrlForApp(`${APP_ID}:${SecurityPageName.detections}`, { + href: getUrlForApp(APP_ID, { + deepLinkId: SecurityPageName.rules, path: getRuleDetailsUrl(params.detailName, !isEmpty(search[0]) ? search[0] : ''), }), }, @@ -95,7 +79,8 @@ export const getBreadcrumbs = ( ...breadcrumb, { text: i18nRules.ADD_PAGE_TITLE, - href: getUrlForApp(`${APP_ID}:${SecurityPageName.detections}`, { + href: getUrlForApp(APP_ID, { + deepLinkId: SecurityPageName.rules, path: getCreateRuleUrl(!isEmpty(search[0]) ? search[0] : ''), }), }, @@ -107,7 +92,8 @@ export const getBreadcrumbs = ( ...breadcrumb, { text: i18nRules.EDIT_PAGE_TITLE, - href: getUrlForApp(`${APP_ID}:${SecurityPageName.detections}`, { + href: getUrlForApp(APP_ID, { + deepLinkId: SecurityPageName.rules, path: getEditRuleUrl(params.detailName, !isEmpty(search[0]) ? search[0] : ''), }), }, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/translations.ts index 4b07369ae3039..9475701baf2fd 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/translations.ts @@ -17,7 +17,7 @@ export const BREADCRUMB_TITLE = i18n.translate( export const PAGE_TITLE = i18n.translate( 'xpack.securitySolution.detectionEngine.detectionsPageTitle', { - defaultMessage: 'Detection alerts', + defaultMessage: 'Alerts', } ); @@ -37,7 +37,7 @@ export const SIGNAL = i18n.translate('xpack.securitySolution.detectionEngine.sig }); export const ALERT = i18n.translate('xpack.securitySolution.detectionEngine.alertTitle', { - defaultMessage: 'Detection alerts', + defaultMessage: 'Alerts', }); export const BUTTON_MANAGE_RULES = i18n.translate( diff --git a/x-pack/plugins/security_solution/public/detections/routes.tsx b/x-pack/plugins/security_solution/public/detections/routes.tsx index e91c612bc49ab..329e1c939c201 100644 --- a/x-pack/plugins/security_solution/public/detections/routes.tsx +++ b/x-pack/plugins/security_solution/public/detections/routes.tsx @@ -6,18 +6,35 @@ */ import React from 'react'; -import { Route, Switch } from 'react-router-dom'; +import { Redirect, RouteProps, RouteComponentProps } from 'react-router-dom'; +import { TrackApplicationView } from '../../../../../src/plugins/usage_collection/public'; +import { ALERTS_PATH, DETECTIONS_PATH, SecurityPageName } from '../../common/constants'; -import { DetectionEngineContainer } from './pages/detection_engine'; -import { NotFoundPage } from '../app/404'; +import { SpyRoute } from '../common/utils/route/spy_routes'; -export const AlertsRoutes: React.FC = () => ( - <Switch> - <Route path="/"> - <DetectionEngineContainer /> - </Route> - <Route> - <NotFoundPage /> - </Route> - </Switch> +import { DetectionEnginePage } from './pages/detection_engine/detection_engine'; + +const renderAlertsRoutes = () => ( + <TrackApplicationView viewId={SecurityPageName.alerts}> + <DetectionEnginePage /> + <SpyRoute pageName={SecurityPageName.alerts} /> + </TrackApplicationView> ); + +const DetectionsRedirects = ({ location }: RouteComponentProps) => + location.pathname === DETECTIONS_PATH ? ( + <Redirect to={{ ...location, pathname: ALERTS_PATH }} /> + ) : ( + <Redirect to={{ ...location, pathname: location.pathname.replace(DETECTIONS_PATH, '') }} /> + ); + +export const routes: RouteProps[] = [ + { + path: ALERTS_PATH, + render: renderAlertsRoutes, + }, + { + path: DETECTIONS_PATH, + component: DetectionsRedirects, + }, +]; diff --git a/x-pack/plugins/security_solution/public/exceptions/index.ts b/x-pack/plugins/security_solution/public/exceptions/index.ts new file mode 100644 index 0000000000000..eccb2ba7578c7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/exceptions/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; + +import { SecuritySubPlugin } from '../app/types'; +import { DETECTIONS_TIMELINE_IDS } from '../detections'; +import { getTimelinesInStorageByIds } from '../timelines/containers/local_storage'; +import { routes } from './routes'; + +export class Exceptions { + public setup() {} + + public start(storage: Storage): SecuritySubPlugin { + return { + storageTimelines: { + timelineById: getTimelinesInStorageByIds(storage, DETECTIONS_TIMELINE_IDS), + }, + routes, + }; + } +} diff --git a/x-pack/plugins/security_solution/public/exceptions/routes.tsx b/x-pack/plugins/security_solution/public/exceptions/routes.tsx new file mode 100644 index 0000000000000..0afc152ed3870 --- /dev/null +++ b/x-pack/plugins/security_solution/public/exceptions/routes.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; + +import { TrackApplicationView } from '../../../../../src/plugins/usage_collection/public'; +import { EXCEPTIONS_PATH, SecurityPageName } from '../../common/constants'; +import { ExceptionListsTable } from '../detections/pages/detection_engine/rules/all/exceptions/exceptions_table'; +import { SpyRoute } from '../common/utils/route/spy_routes'; + +export const ExceptionsRoutes = () => { + return ( + <TrackApplicationView viewId={SecurityPageName.exceptions}> + <ExceptionListsTable /> + <SpyRoute pageName={SecurityPageName.exceptions} /> + </TrackApplicationView> + ); +}; + +export const routes = [ + { + path: EXCEPTIONS_PATH, + render: ExceptionsRoutes, + }, +]; diff --git a/x-pack/plugins/security_solution/public/helpers.ts b/x-pack/plugins/security_solution/public/helpers.ts index e12b2f7fc37f8..d941550fdb2e3 100644 --- a/x-pack/plugins/security_solution/public/helpers.ts +++ b/x-pack/plugins/security_solution/public/helpers.ts @@ -6,9 +6,10 @@ */ import { isEmpty } from 'lodash/fp'; +import { matchPath } from 'react-router-dom'; import { CoreStart } from '../../../../src/core/public'; -import { APP_ID } from '../common/constants'; +import { ALERTS_PATH, APP_ID, EXCEPTIONS_PATH, RULES_PATH } from '../common/constants'; import { FactoryQueryTypes, StrategyResponseType, @@ -47,61 +48,85 @@ export const parseRoute = (location: Pick<Location, 'hash' | 'pathname' | 'searc export const manageOldSiemRoutes = async (coreStart: CoreStart) => { const { application } = coreStart; - const { pageName, path, search } = parseRoute(window.location); + const { pageName, path } = parseRoute(window.location); switch (pageName) { case SecurityPageName.overview: - application.navigateToApp(`${APP_ID}:${SecurityPageName.overview}`, { + application.navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.overview, replace: true, path, }); break; case 'ml-hosts': - application.navigateToApp(`${APP_ID}:${SecurityPageName.hosts}`, { + application.navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.hosts, replace: true, path: `/ml-hosts${path}`, }); break; case SecurityPageName.hosts: - application.navigateToApp(`${APP_ID}:${SecurityPageName.hosts}`, { + application.navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.hosts, replace: true, path, }); break; case 'ml-network': - application.navigateToApp(`${APP_ID}:${SecurityPageName.network}`, { + application.navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.network, replace: true, path: `/ml-network${path}`, }); break; case SecurityPageName.network: - application.navigateToApp(`${APP_ID}:${SecurityPageName.network}`, { + application.navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.network, replace: true, path, }); break; case SecurityPageName.timelines: - application.navigateToApp(`${APP_ID}:${SecurityPageName.timelines}`, { + application.navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.timelines, replace: true, path, }); break; case SecurityPageName.case: - application.navigateToApp(`${APP_ID}:${SecurityPageName.case}`, { + application.navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.case, replace: true, path, }); break; - case 'detections': - application.navigateToApp(`${APP_ID}:${SecurityPageName.detections}`, { + case SecurityPageName.detections: + case SecurityPageName.alerts: + application.navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.alerts, + replace: true, + path, + }); + break; + case SecurityPageName.rules: + application.navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.rules, + replace: true, + path, + }); + break; + case SecurityPageName.exceptions: + application.navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.exceptions, replace: true, path, }); break; default: - application.navigateToApp(`${APP_ID}:${SecurityPageName.overview}`, { + application.navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.overview, replace: true, - path: `${search}`, + path, }); break; } @@ -115,3 +140,10 @@ export const getInspectResponse = <T extends FactoryQueryTypes>( response: response != null ? [JSON.stringify(response.rawResponse, null, 2)] : prevResponse?.response, }); + +export const isDetectionsPath = (pathname: string): boolean => { + return !!matchPath(pathname, { + path: `(${ALERTS_PATH}|${RULES_PATH}|${EXCEPTIONS_PATH})`, + strict: false, + }); +}; diff --git a/x-pack/plugins/security_solution/public/hosts/index.ts b/x-pack/plugins/security_solution/public/hosts/index.ts index 64b805bf5e2d6..cbb539f8e4107 100644 --- a/x-pack/plugins/security_solution/public/hosts/index.ts +++ b/x-pack/plugins/security_solution/public/hosts/index.ts @@ -9,7 +9,7 @@ import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { TimelineIdLiteral, TimelineId } from '../../common/types/timeline'; import { SecuritySubPluginWithStore } from '../app/types'; import { getTimelinesInStorageByIds } from '../timelines/containers/local_storage'; -import { HostsRoutes } from './routes'; +import { routes } from './routes'; import { initialHostsState, hostsReducer, HostsState } from './store'; const HOST_TIMELINE_IDS: TimelineIdLiteral[] = [ @@ -22,7 +22,7 @@ export class Hosts { public start(storage: Storage): SecuritySubPluginWithStore<'hosts', HostsState> { return { - SubPluginRoutes: HostsRoutes, + routes, storageTimelines: { timelineById: getTimelinesInStorageByIds(storage, HOST_TIMELINE_IDS), }, diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx index b51e20b801f40..3b76ec8a0d13f 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.test.tsx @@ -80,7 +80,7 @@ describe('body', () => { test(`it should pass expected object properties to ${componentName}`, () => { const wrapper = mount( <TestProviders> - <MemoryRouter initialEntries={[`/host-1/${path}`]}> + <MemoryRouter initialEntries={[`/hosts/host-1/${path}`]}> <HostDetailsTabs isInitializing={false} detailName={'host-1'} diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/nav_tabs.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/nav_tabs.tsx index 2f6d5e5bcdc33..02f8fa740c024 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/nav_tabs.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/nav_tabs.tsx @@ -9,10 +9,10 @@ import { omit } from 'lodash/fp'; import * as i18n from '../translations'; import { HostDetailsNavTab } from './types'; import { HostsTableType } from '../../store/model'; -import { SecurityPageName } from '../../../app/types'; +import { HOSTS_PATH } from '../../../../common/constants'; const getTabsOnHostDetailsUrl = (hostName: string, tabName: HostsTableType) => - `/${hostName}/${tabName}`; + `${HOSTS_PATH}/${hostName}/${tabName}`; export const navTabsHostDetails = ( hostName: string, @@ -24,44 +24,30 @@ export const navTabsHostDetails = ( name: i18n.NAVIGATION_AUTHENTICATIONS_TITLE, href: getTabsOnHostDetailsUrl(hostName, HostsTableType.authentications), disabled: false, - urlKey: 'host', - isDetailPage: true, - pageId: SecurityPageName.hosts, }, [HostsTableType.uncommonProcesses]: { id: HostsTableType.uncommonProcesses, name: i18n.NAVIGATION_UNCOMMON_PROCESSES_TITLE, href: getTabsOnHostDetailsUrl(hostName, HostsTableType.uncommonProcesses), disabled: false, - urlKey: 'host', - isDetailPage: true, - pageId: SecurityPageName.hosts, }, [HostsTableType.anomalies]: { id: HostsTableType.anomalies, name: i18n.NAVIGATION_ANOMALIES_TITLE, href: getTabsOnHostDetailsUrl(hostName, HostsTableType.anomalies), disabled: false, - urlKey: 'host', - isDetailPage: true, - pageId: SecurityPageName.hosts, }, [HostsTableType.events]: { id: HostsTableType.events, name: i18n.NAVIGATION_EVENTS_TITLE, href: getTabsOnHostDetailsUrl(hostName, HostsTableType.events), disabled: false, - urlKey: 'host', - isDetailPage: true, - pageId: SecurityPageName.hosts, }, [HostsTableType.alerts]: { id: HostsTableType.alerts, name: i18n.NAVIGATION_ALERTS_TITLE, href: getTabsOnHostDetailsUrl(hostName, HostsTableType.alerts), disabled: false, - urlKey: 'host', - pageId: SecurityPageName.hosts, }, }; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/utils.ts b/x-pack/plugins/security_solution/public/hosts/pages/details/utils.ts index 5e3541feeb0aa..f4e14605cab47 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/utils.ts +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/utils.ts @@ -37,8 +37,9 @@ export const getBreadcrumbs = ( let breadcrumb = [ { text: i18n.PAGE_TITLE, - href: getUrlForApp(`${APP_ID}:${SecurityPageName.hosts}`, { + href: getUrlForApp(APP_ID, { path: !isEmpty(search[0]) ? search[0] : '', + deepLinkId: SecurityPageName.hosts, }), }, ]; @@ -48,12 +49,14 @@ export const getBreadcrumbs = ( ...breadcrumb, { text: params.detailName, - href: getUrlForApp(`${APP_ID}:${SecurityPageName.hosts}`, { + href: getUrlForApp(APP_ID, { path: getHostDetailsUrl(params.detailName, !isEmpty(search[0]) ? search[0] : ''), + deepLinkId: SecurityPageName.hosts, }), }, ]; } + if (params.tabName != null) { const tabName = get('tabName', params); if (!tabName) return breadcrumb; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts_tabs.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts_tabs.tsx index 67a2b9419f9b8..876730a8f66c4 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts_tabs.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts_tabs.tsx @@ -15,7 +15,7 @@ import { HostsTableType } from '../store/model'; import { AnomaliesQueryTabBody } from '../../common/containers/anomalies/anomalies_query_tab_body'; import { AnomaliesHostTable } from '../../common/components/ml/tables/anomalies_host_table'; import { UpdateDateRange } from '../../common/components/charts/common'; - +import { HOSTS_PATH } from '../../../common/constants'; import { HostsQueryTabBody, AuthenticationsQueryTabBody, @@ -79,22 +79,22 @@ export const HostsTabs = memo<HostsTabsProps>( return ( <Switch> - <Route path={`/:tabName(${HostsTableType.hosts})`}> + <Route path={`${HOSTS_PATH}/:tabName(${HostsTableType.hosts})`}> <HostsQueryTabBody docValueFields={docValueFields} {...tabProps} /> </Route> - <Route path={`/:tabName(${HostsTableType.authentications})`}> + <Route path={`${HOSTS_PATH}/:tabName(${HostsTableType.authentications})`}> <AuthenticationsQueryTabBody docValueFields={docValueFields} {...tabProps} /> </Route> - <Route path={`/:tabName(${HostsTableType.uncommonProcesses})`}> + <Route path={`${HOSTS_PATH}/:tabName(${HostsTableType.uncommonProcesses})`}> <UncommonProcessQueryTabBody {...tabProps} /> </Route> - <Route path={`/:tabName(${HostsTableType.anomalies})`}> + <Route path={`${HOSTS_PATH}/:tabName(${HostsTableType.anomalies})`}> <AnomaliesQueryTabBody {...tabProps} AnomaliesTableComponent={AnomaliesHostTable} /> </Route> - <Route path={`/:tabName(${HostsTableType.events})`}> + <Route path={`${HOSTS_PATH}/:tabName(${HostsTableType.events})`}> <EventsQueryTabBody {...tabProps} /> </Route> - <Route path={`/:tabName(${HostsTableType.alerts})`}> + <Route path={`${HOSTS_PATH}/:tabName(${HostsTableType.alerts})`}> <HostAlertsQueryTabBody {...tabProps} /> </Route> </Switch> diff --git a/x-pack/plugins/security_solution/public/hosts/pages/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/index.tsx index e4f447528ead6..23be8f09ce140 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/index.tsx @@ -6,17 +6,17 @@ */ import React from 'react'; -import { Route, Switch, RouteComponentProps, useHistory } from 'react-router-dom'; - +import { Route, Switch, Redirect } from 'react-router-dom'; +import { HOSTS_PATH } from '../../../common/constants'; import { HostDetails } from './details'; import { HostsTableType } from '../store/model'; import { MlHostConditionalContainer } from '../../common/components/ml/conditional_links/ml_host_conditional_container'; import { Hosts } from './hosts'; -import { hostsPagePath, hostDetailsPagePath } from './types'; +import { hostDetailsPagePath } from './types'; const getHostsTabPath = () => - `/:tabName(` + + `${HOSTS_PATH}/:tabName(` + `${HostsTableType.hosts}|` + `${HostsTableType.authentications}|` + `${HostsTableType.uncommonProcesses}|` + @@ -24,7 +24,7 @@ const getHostsTabPath = () => `${HostsTableType.events}|` + `${HostsTableType.alerts})`; -const getHostDetailsTabPath = (pagePath: string) => +const getHostDetailsTabPath = () => `${hostDetailsPagePath}/:tabName(` + `${HostsTableType.authentications}|` + `${HostsTableType.uncommonProcesses}|` + @@ -32,24 +32,26 @@ const getHostDetailsTabPath = (pagePath: string) => `${HostsTableType.events}|` + `${HostsTableType.alerts})`; -type Props = Partial<RouteComponentProps<{}>> & { url: string }; - -export const HostsContainer = React.memo<Props>(({ url }) => { - const history = useHistory(); - +export const HostsContainer = React.memo(() => { return ( <Switch> <Route - path="/ml-hosts" - render={({ location, match }) => ( - <MlHostConditionalContainer location={location} url={match.url} /> + exact + strict + path={HOSTS_PATH} + render={({ location: { search = '' } }) => ( + <Redirect to={{ pathname: `${HOSTS_PATH}/${HostsTableType.hosts}`, search }} /> )} /> + + <Route path={`${HOSTS_PATH}/ml-hosts`}> + <MlHostConditionalContainer /> + </Route> <Route path={getHostsTabPath()}> <Hosts /> </Route> <Route - path={getHostDetailsTabPath(hostsPagePath)} + path={getHostDetailsTabPath()} render={({ match: { params: { detailName }, @@ -63,20 +65,14 @@ export const HostsContainer = React.memo<Props>(({ url }) => { params: { detailName }, }, location: { search = '' }, - }) => { - history.replace(`${detailName}/${HostsTableType.authentications}${search}`); - return null; - }} - /> - - <Route - exact - strict - path="" - render={({ location: { search = '' } }) => { - history.replace(`${HostsTableType.hosts}${search}`); - return null; - }} + }) => ( + <Redirect + to={{ + pathname: `${HOSTS_PATH}/${detailName}/${HostsTableType.authentications}`, + search, + }} + /> + )} /> </Switch> ); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/nav_tabs.tsx b/x-pack/plugins/security_solution/public/hosts/pages/nav_tabs.tsx index 41f0dda8ea0cc..c43a6431d2da8 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/nav_tabs.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/nav_tabs.tsx @@ -9,9 +9,9 @@ import { omit } from 'lodash/fp'; import * as i18n from './translations'; import { HostsTableType } from '../store/model'; import { HostsNavTab } from './navigation/types'; -import { SecurityPageName } from '../../app/types'; +import { HOSTS_PATH } from '../../../common/constants'; -const getTabsOnHostsUrl = (tabName: HostsTableType) => `/${tabName}`; +const getTabsOnHostsUrl = (tabName: HostsTableType) => `${HOSTS_PATH}/${tabName}`; export const navTabsHosts = (hasMlUserPermissions: boolean): HostsNavTab => { const hostsNavTabs = { @@ -20,48 +20,36 @@ export const navTabsHosts = (hasMlUserPermissions: boolean): HostsNavTab => { name: i18n.NAVIGATION_ALL_HOSTS_TITLE, href: getTabsOnHostsUrl(HostsTableType.hosts), disabled: false, - urlKey: 'host', - pageId: SecurityPageName.hosts, }, [HostsTableType.authentications]: { id: HostsTableType.authentications, name: i18n.NAVIGATION_AUTHENTICATIONS_TITLE, href: getTabsOnHostsUrl(HostsTableType.authentications), disabled: false, - urlKey: 'host', - pageId: SecurityPageName.hosts, }, [HostsTableType.uncommonProcesses]: { id: HostsTableType.uncommonProcesses, name: i18n.NAVIGATION_UNCOMMON_PROCESSES_TITLE, href: getTabsOnHostsUrl(HostsTableType.uncommonProcesses), disabled: false, - urlKey: 'host', - pageId: SecurityPageName.hosts, }, [HostsTableType.anomalies]: { id: HostsTableType.anomalies, name: i18n.NAVIGATION_ANOMALIES_TITLE, href: getTabsOnHostsUrl(HostsTableType.anomalies), disabled: false, - urlKey: 'host', - pageId: SecurityPageName.hosts, }, [HostsTableType.events]: { id: HostsTableType.events, name: i18n.NAVIGATION_EVENTS_TITLE, href: getTabsOnHostsUrl(HostsTableType.events), disabled: false, - urlKey: 'host', - pageId: SecurityPageName.hosts, }, [HostsTableType.alerts]: { id: HostsTableType.alerts, name: i18n.NAVIGATION_ALERTS_TITLE, href: getTabsOnHostsUrl(HostsTableType.alerts), disabled: false, - urlKey: 'host', - pageId: SecurityPageName.hosts, }, }; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/types.ts b/x-pack/plugins/security_solution/public/hosts/pages/types.ts index f810a73d12596..c7fd743ee5e44 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/types.ts +++ b/x-pack/plugins/security_solution/public/hosts/pages/types.ts @@ -11,9 +11,9 @@ import { hostsModel } from '../store'; import { GlobalTimeArgs } from '../../common/containers/use_global_time'; import { InputsModelId } from '../../common/store/inputs/constants'; import { DocValueFields } from '../../common/containers/source'; +import { HOSTS_PATH } from '../../../common/constants'; -export const hostsPagePath = '/'; -export const hostDetailsPagePath = `/:detailName`; +export const hostDetailsPagePath = `${HOSTS_PATH}/:detailName`; export type HostsTabsProps = GlobalTimeArgs & { docValueFields: DocValueFields[]; diff --git a/x-pack/plugins/security_solution/public/hosts/routes.tsx b/x-pack/plugins/security_solution/public/hosts/routes.tsx index e15acdebeeb40..1eeddf2155613 100644 --- a/x-pack/plugins/security_solution/public/hosts/routes.tsx +++ b/x-pack/plugins/security_solution/public/hosts/routes.tsx @@ -6,14 +6,20 @@ */ import React from 'react'; -import { Route, Switch } from 'react-router-dom'; - import { HostsContainer } from './pages'; -import { NotFoundPage } from '../app/404'; +import { SecurityPageName, SecuritySubPluginRoutes } from '../app/types'; +import { TrackApplicationView } from '../../../../../src/plugins/usage_collection/public'; +import { HOSTS_PATH } from '../../common/constants'; export const HostsRoutes = () => ( - <Switch> - <Route path="/" render={({ match }) => <HostsContainer url={match.url} />} /> - <Route render={() => <NotFoundPage />} /> - </Switch> + <TrackApplicationView viewId={SecurityPageName.hosts}> + <HostsContainer /> + </TrackApplicationView> ); + +export const routes: SecuritySubPluginRoutes = [ + { + path: HOSTS_PATH, + render: HostsRoutes, + }, +]; diff --git a/x-pack/plugins/security_solution/public/lazy_application_dependencies.tsx b/x-pack/plugins/security_solution/public/lazy_application_dependencies.tsx index 536d1d084f0c5..daf2c55a44333 100644 --- a/x-pack/plugins/security_solution/public/lazy_application_dependencies.tsx +++ b/x-pack/plugins/security_solution/public/lazy_application_dependencies.tsx @@ -13,5 +13,4 @@ import { renderApp } from './app'; import { createStore, createInitialState } from './common/store'; - export { renderApp, createStore, createInitialState }; diff --git a/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx b/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx index c36900a7ce8d5..47026cbec49ad 100644 --- a/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx +++ b/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx @@ -10,11 +10,15 @@ * By loading these later we can reduce the initial bundle size and allow users to delay loading these dependencies until they are needed. */ -import { Detections } from './detections'; import { Cases } from './cases'; +import { Detections } from './detections'; +import { Exceptions } from './exceptions'; + import { Hosts } from './hosts'; import { Network } from './network'; import { Overview } from './overview'; +import { Rules } from './rules'; + import { Timelines } from './timelines'; import { Management } from './management'; @@ -24,9 +28,11 @@ import { Management } from './management'; const subPluginClasses = { Detections, Cases, + Exceptions, Hosts, Network, Overview, + Rules, Timelines, Management, }; diff --git a/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts b/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts index 3bcbd81621588..d437c45792766 100644 --- a/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts +++ b/x-pack/plugins/security_solution/public/management/common/breadcrumbs.ts @@ -29,7 +29,8 @@ export function getBreadcrumbs( return [ { text: ADMINISTRATION, - href: getUrlForApp(`${APP_ID}:${SecurityPageName.administration}`, { + href: getUrlForApp(APP_ID, { + deepLinkId: SecurityPageName.endpoints, path: !isEmpty(search[0]) ? search[0] : '', }), }, diff --git a/x-pack/plugins/security_solution/public/management/common/constants.ts b/x-pack/plugins/security_solution/public/management/common/constants.ts index 2ed00309992a8..f6b147d729322 100644 --- a/x-pack/plugins/security_solution/public/management/common/constants.ts +++ b/x-pack/plugins/security_solution/public/management/common/constants.ts @@ -5,18 +5,15 @@ * 2.0. */ +import { MANAGEMENT_PATH } from '../../../common/constants'; import { ManagementStoreGlobalNamespace, AdministrationSubTab } from '../types'; -import { APP_ID } from '../../../common/constants'; -import { SecurityPageName } from '../../app/types'; // --[ ROUTING ]--------------------------------------------------------------------------- -export const MANAGEMENT_APP_ID = `${APP_ID}:${SecurityPageName.administration}`; -export const MANAGEMENT_ROUTING_ROOT_PATH = ''; -export const MANAGEMENT_ROUTING_ENDPOINTS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${AdministrationSubTab.endpoints})`; -export const MANAGEMENT_ROUTING_POLICIES_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${AdministrationSubTab.policies})`; -export const MANAGEMENT_ROUTING_POLICY_DETAILS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${AdministrationSubTab.policies})/:policyId`; -export const MANAGEMENT_ROUTING_TRUSTED_APPS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${AdministrationSubTab.trustedApps})`; -export const MANAGEMENT_ROUTING_EVENT_FILTERS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${AdministrationSubTab.eventFilters})`; +export const MANAGEMENT_ROUTING_ENDPOINTS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.endpoints})`; +export const MANAGEMENT_ROUTING_POLICIES_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.policies})`; +export const MANAGEMENT_ROUTING_POLICY_DETAILS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.policies})/:policyId`; +export const MANAGEMENT_ROUTING_TRUSTED_APPS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.trustedApps})`; +export const MANAGEMENT_ROUTING_EVENT_FILTERS_PATH = `${MANAGEMENT_PATH}/:tabName(${AdministrationSubTab.eventFilters})`; // --[ STORE ]--------------------------------------------------------------------------- /** The SIEM global store namespace where the management state will be mounted */ diff --git a/x-pack/plugins/security_solution/public/management/common/routing.test.ts b/x-pack/plugins/security_solution/public/management/common/routing.test.ts index a1662c21012be..82b7a15d642e4 100644 --- a/x-pack/plugins/security_solution/public/management/common/routing.test.ts +++ b/x-pack/plugins/security_solution/public/management/common/routing.test.ts @@ -98,27 +98,35 @@ describe('routing', () => { describe('getTrustedAppsListPath()', () => { it('builds proper path when no parameters provided', () => { - expect(getTrustedAppsListPath()).toEqual('/trusted_apps'); + expect(getTrustedAppsListPath()).toEqual('/administration/trusted_apps'); }); it('builds proper path when empty parameters provided', () => { - expect(getTrustedAppsListPath({})).toEqual('/trusted_apps'); + expect(getTrustedAppsListPath({})).toEqual('/administration/trusted_apps'); }); it('builds proper path when only page size provided', () => { - expect(getTrustedAppsListPath({ page_size: 20 })).toEqual('/trusted_apps?page_size=20'); + expect(getTrustedAppsListPath({ page_size: 20 })).toEqual( + '/administration/trusted_apps?page_size=20' + ); }); it('builds proper path when only page index provided', () => { - expect(getTrustedAppsListPath({ page_index: 2 })).toEqual('/trusted_apps?page_index=2'); + expect(getTrustedAppsListPath({ page_index: 2 })).toEqual( + '/administration/trusted_apps?page_index=2' + ); }); it('builds proper path when only "show" provided', () => { - expect(getTrustedAppsListPath({ show: 'create' })).toEqual('/trusted_apps?show=create'); + expect(getTrustedAppsListPath({ show: 'create' })).toEqual( + '/administration/trusted_apps?show=create' + ); }); it('builds proper path when only view type provided', () => { - expect(getTrustedAppsListPath({ view_type: 'list' })).toEqual('/trusted_apps?view_type=list'); + expect(getTrustedAppsListPath({ view_type: 'list' })).toEqual( + '/administration/trusted_apps?view_type=list' + ); }); it('builds proper path when all params provided', () => { @@ -131,7 +139,7 @@ describe('routing', () => { }; expect(getTrustedAppsListPath(location)).toEqual( - '/trusted_apps?page_index=2&page_size=20&view_type=list&show=create' + '/administration/trusted_apps?page_index=2&page_size=20&view_type=list&show=create' ); }); @@ -143,7 +151,7 @@ describe('routing', () => { view_type: 'list', }); - expect(path).toEqual('/trusted_apps?page_size=20&view_type=list&show=create'); + expect(path).toEqual('/administration/trusted_apps?page_size=20&view_type=list&show=create'); }); it('builds proper path when page size is equal to default', () => { @@ -154,7 +162,7 @@ describe('routing', () => { view_type: 'list', }); - expect(path).toEqual('/trusted_apps?page_index=2&view_type=list&show=create'); + expect(path).toEqual('/administration/trusted_apps?page_index=2&view_type=list&show=create'); }); it('builds proper path when "show" is equal to default', () => { @@ -165,7 +173,7 @@ describe('routing', () => { view_type: 'list', }); - expect(path).toEqual('/trusted_apps?page_index=2&page_size=20&view_type=list'); + expect(path).toEqual('/administration/trusted_apps?page_index=2&page_size=20&view_type=list'); }); it('builds proper path when view type is equal to default', () => { @@ -176,7 +184,7 @@ describe('routing', () => { view_type: 'grid', }); - expect(path).toEqual('/trusted_apps?page_index=2&page_size=20&show=create'); + expect(path).toEqual('/administration/trusted_apps?page_index=2&page_size=20&show=create'); }); it('builds proper path when params are equal to default', () => { @@ -187,7 +195,7 @@ describe('routing', () => { view_type: 'grid', }); - expect(path).toEqual('/trusted_apps'); + expect(path).toEqual('/administration/trusted_apps'); }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx b/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx index 021c900824f8d..09b47b76c486f 100644 --- a/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx +++ b/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx @@ -6,25 +6,13 @@ */ import React, { FC, memo } from 'react'; -import { EuiPanel, EuiSpacer, CommonProps } from '@elastic/eui'; +import { EuiPanel, CommonProps } from '@elastic/eui'; import styled from 'styled-components'; import { SecurityPageName } from '../../../common/constants'; import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper'; import { HeaderPage } from '../../common/components/header_page'; -import { SecuritySolutionTabNavigation } from '../../common/components/navigation'; import { SpyRoute } from '../../common/utils/route/spy_routes'; -import { AdministrationSubTab } from '../types'; -import { - ENDPOINTS_TAB, - TRUSTED_APPS_TAB, - BETA_BADGE_LABEL, - EVENT_FILTERS_TAB, -} from '../common/translations'; -import { - getEndpointListPath, - getEventFiltersListPath, - getTrustedAppsListPath, -} from '../common/routing'; +import { BETA_BADGE_LABEL } from '../common/translations'; /** Ensure that all flyouts z-index in Administation area show the flyout header */ const EuiPanelStyled = styled(EuiPanel)` @@ -57,37 +45,6 @@ export const AdministrationListPage: FC<AdministrationListPageProps & CommonProp {actions} </HeaderPage> - <SecuritySolutionTabNavigation - navTabs={{ - [AdministrationSubTab.endpoints]: { - name: ENDPOINTS_TAB, - id: AdministrationSubTab.endpoints, - href: getEndpointListPath({ name: 'endpointList' }), - urlKey: 'administration', - pageId: SecurityPageName.administration, - disabled: false, - }, - [AdministrationSubTab.trustedApps]: { - name: TRUSTED_APPS_TAB, - id: AdministrationSubTab.trustedApps, - href: getTrustedAppsListPath(), - urlKey: 'administration', - pageId: SecurityPageName.administration, - disabled: false, - }, - [AdministrationSubTab.eventFilters]: { - name: EVENT_FILTERS_TAB, - id: AdministrationSubTab.eventFilters, - href: getEventFiltersListPath(), - urlKey: 'administration', - pageId: SecurityPageName.administration, - disabled: false, - }, - }} - /> - - <EuiSpacer /> - <EuiPanelStyled hasBorder>{children}</EuiPanelStyled> <SpyRoute pageName={SecurityPageName.administration} /> diff --git a/x-pack/plugins/security_solution/public/management/components/hooks/use_management_format_url.ts b/x-pack/plugins/security_solution/public/management/components/hooks/use_management_format_url.ts deleted file mode 100644 index a90794a075b45..0000000000000 --- a/x-pack/plugins/security_solution/public/management/components/hooks/use_management_format_url.ts +++ /dev/null @@ -1,19 +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 { useKibana } from '../../../common/lib/kibana'; -import { MANAGEMENT_APP_ID } from '../../common/constants'; - -/** - * Returns a full URL to the provided Management page path by using - * kibana's `getUrlForApp()` - * - * @param managementPath - */ -export const useManagementFormatUrl = (managementPath: string) => { - return `${useKibana().services.application.getUrlForApp(MANAGEMENT_APP_ID)}${managementPath}`; -}; diff --git a/x-pack/plugins/security_solution/public/management/index.ts b/x-pack/plugins/security_solution/public/management/index.ts index 2859adae97d41..326f8471aa621 100644 --- a/x-pack/plugins/security_solution/public/management/index.ts +++ b/x-pack/plugins/security_solution/public/management/index.ts @@ -7,7 +7,7 @@ import { CoreStart } from 'kibana/public'; import { Reducer, CombinedState } from 'redux'; -import { ManagementRoutes } from './routes'; +import { routes } from './routes'; import { StartPlugins } from '../types'; import { SecuritySubPluginWithStore } from '../app/types'; import { managementReducer } from './store/reducer'; @@ -39,7 +39,7 @@ export class Management { plugins: StartPlugins ): SecuritySubPluginWithStore<'management', ManagementState> { return { - SubPluginRoutes: ManagementRoutes, + routes, store: { initialState: { management: undefined, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.test.tsx index a860e3c45deee..0b5ff7cc4da0f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_agent_status.test.tsx @@ -41,7 +41,7 @@ describe('When using the EndpointAgentStatus component', () => { }; act(() => { - mockedContext.history.push('/endpoints'); + mockedContext.history.push('/administration/endpoints'); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_policy_link.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_policy_link.tsx index c152da1029395..2919fdd15e29d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_policy_link.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/endpoint_policy_link.tsx @@ -10,9 +10,8 @@ import { EuiLink, EuiLinkAnchorProps } from '@elastic/eui'; import { useEndpointSelector } from '../hooks'; import { nonExistingPolicies } from '../../store/selectors'; import { getPolicyDetailPath } from '../../../../common/routing'; -import { useFormatUrl } from '../../../../../common/components/link_to'; -import { SecurityPageName } from '../../../../../../common/constants'; import { useNavigateByRouterEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; +import { useAppUrl } from '../../../../../common/lib/kibana/hooks'; /** * A policy link (to details) that first checks to see if the policy id exists against @@ -25,14 +24,14 @@ export const EndpointPolicyLink = memo< } >(({ policyId, children, onClick, ...otherProps }) => { const missingPolicies = useEndpointSelector(nonExistingPolicies); - const { formatUrl } = useFormatUrl(SecurityPageName.administration); + const { getAppUrl } = useAppUrl(); const { toRoutePath, toRouteUrl } = useMemo(() => { - const toPath = getPolicyDetailPath(policyId); + const path = getPolicyDetailPath(policyId); return { - toRoutePath: toPath, - toRouteUrl: formatUrl(toPath), + toRoutePath: path, + toRouteUrl: getAppUrl({ path }), }; - }, [formatUrl, policyId]); + }, [policyId, getAppUrl]); const clickHandler = useNavigateByRouterEventHandler(toRoutePath, onClick); if (missingPolicies[policyId]) { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.test.tsx index 04708ea90cd34..04fd8cd715c87 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/actions_menu.test.tsx @@ -17,7 +17,21 @@ import { endpointPageHttpMock } from '../../../mocks'; import { fireEvent } from '@testing-library/dom'; import { licenseService } from '../../../../../../common/hooks/use_license'; -jest.mock('../../../../../../common/lib/kibana'); +jest.mock('../../../../../../common/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(), + }, + }, + }), + }; +}); jest.mock('../../../../../../common/hooks/use_license'); describe('When using the Endpoint Details Actions Menu', () => { @@ -45,7 +59,7 @@ describe('When using the Endpoint Details Actions Menu', () => { act(() => { mockedContext.history.push( - '/endpoints?selected_endpoint=5fe11314-678c-413e-87a2-b4a3461878ee' + '/administration/endpoints?selected_endpoint=5fe11314-678c-413e-87a2-b4a3461878ee' ); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/back_to_endpoint_details_flyout_subheader.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/back_to_endpoint_details_flyout_subheader.tsx index 7218e794f587a..6c9ae7631e6d3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/back_to_endpoint_details_flyout_subheader.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/back_to_endpoint_details_flyout_subheader.tsx @@ -10,14 +10,13 @@ import { i18n } from '@kbn/i18n'; import { FlyoutSubHeader, FlyoutSubHeaderProps } from './flyout_sub_header'; import { getEndpointDetailsPath } from '../../../../../common/routing'; import { useNavigateByRouterEventHandler } from '../../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; -import { useFormatUrl } from '../../../../../../common/components/link_to'; -import { SecurityPageName } from '../../../../../../../common/constants'; import { useEndpointSelector } from '../../hooks'; import { uiQueryParams } from '../../../store/selectors'; +import { useAppUrl } from '../../../../../../common/lib/kibana/hooks'; export const BackToEndpointDetailsFlyoutSubHeader = memo<{ endpointId: string }>( ({ endpointId }) => { - const { formatUrl } = useFormatUrl(SecurityPageName.administration); + const { getAppUrl } = useAppUrl(); const { show, ...currentUrlQueryParams } = useEndpointSelector(uiQueryParams); const detailsRoutePath = useMemo( @@ -37,10 +36,10 @@ export const BackToEndpointDetailsFlyoutSubHeader = memo<{ endpointId: string }> title: i18n.translate('xpack.securitySolution.endpoint.policyResponse.backLinkTitle', { defaultMessage: 'Endpoint Details', }), - href: formatUrl(detailsRoutePath), + href: getAppUrl({ path: detailsRoutePath }), onClick: backToDetailsClickHandler, }; - }, [backToDetailsClickHandler, detailsRoutePath, formatUrl]); + }, [backToDetailsClickHandler, getAppUrl, detailsRoutePath]); return ( <FlyoutSubHeader diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx index 64ea575c37d79..369b4c128e052 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx @@ -27,11 +27,10 @@ import { POLICY_STATUS_TO_BADGE_COLOR } from '../host_constants'; import { FormattedDateAndTime } from '../../../../../common/components/endpoint/formatted_date_time'; import { useNavigateByRouterEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; import { getEndpointDetailsPath } from '../../../../common/routing'; -import { SecurityPageName } from '../../../../../app/types'; -import { useFormatUrl } from '../../../../../common/components/link_to'; import { EndpointPolicyLink } from '../components/endpoint_policy_link'; import { OutOfDate } from '../components/out_of_date'; import { EndpointAgentStatus } from '../components/endpoint_agent_status'; +import { useAppUrl } from '../../../../../common/lib/kibana/hooks'; const HostIds = styled(EuiListGroupItem)` margin-top: 0; @@ -54,26 +53,18 @@ export const EndpointDetails = memo( const policyStatus = useEndpointSelector( policyResponseStatus ) as keyof typeof POLICY_STATUS_TO_BADGE_COLOR; - const { formatUrl } = useFormatUrl(SecurityPageName.administration); + const { getAppUrl } = useAppUrl(); const [policyResponseUri, policyResponseRoutePath] = useMemo(() => { // eslint-disable-next-line @typescript-eslint/naming-convention const { selected_endpoint, show, ...currentUrlParams } = queryParams; - return [ - formatUrl( - getEndpointDetailsPath({ - name: 'endpointPolicyResponse', - ...currentUrlParams, - selected_endpoint: details.agent.id, - }) - ), - getEndpointDetailsPath({ - name: 'endpointPolicyResponse', - ...currentUrlParams, - selected_endpoint: details.agent.id, - }), - ]; - }, [details.agent.id, formatUrl, queryParams]); + const path = getEndpointDetailsPath({ + name: 'endpointPolicyResponse', + ...currentUrlParams, + selected_endpoint: details.agent.id, + }); + return [getAppUrl({ path }), path]; + }, [details.agent.id, getAppUrl, queryParams]); const policyStatusClickHandler = useNavigateByRouterEventHandler(policyResponseRoutePath); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/hooks.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/hooks.ts index 4c00c00e50dbc..ca14dde18455b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/hooks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/hooks.ts @@ -13,7 +13,7 @@ import { MANAGEMENT_STORE_ENDPOINTS_NAMESPACE, MANAGEMENT_STORE_GLOBAL_NAMESPACE, } from '../../../../common/constants'; -import { useKibana } from '../../../../../common/lib/kibana'; +import { useAppUrl } from '../../../../../common/lib/kibana'; export function useEndpointSelector<TSelected>(selector: (state: EndpointState) => TSelected) { return useSelector(function (state: State) { @@ -29,15 +29,15 @@ export function useEndpointSelector<TSelected>(selector: (state: EndpointState) * Returns an object that contains Fleet app and URL information */ export const useIngestUrl = (subpath: string): { url: string; appId: string; appPath: string } => { - const { services } = useKibana(); + const { getAppUrl } = useAppUrl(); return useMemo(() => { const appPath = `#/${subpath}`; return { - url: `${services.application.getUrlForApp('fleet')}${appPath}`, + url: `${getAppUrl({ appId: 'fleet' })}${appPath}`, appId: 'fleet', appPath, }; - }, [services.application, subpath]); + }, [getAppUrl, subpath]); }; /** * Returns an object that contains Fleet app and URL information @@ -45,13 +45,13 @@ export const useIngestUrl = (subpath: string): { url: string; appId: string; app export const useAgentDetailsIngestUrl = ( agentId: string ): { url: string; appId: string; appPath: string } => { - const { services } = useKibana(); + const { getAppUrl } = useAppUrl(); return useMemo(() => { const appPath = `#/fleet/agents/${agentId}/activity`; return { - url: `${services.application.getUrlForApp('fleet')}${appPath}`, + url: `${getAppUrl({ appId: 'fleet' })}${appPath}`, appId: 'fleet', appPath, }; - }, [services.application, agentId]); + }, [getAppUrl, agentId]); }; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx index 408e1794ef680..0422cbcaa4310 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/hooks/use_endpoint_action_items.tsx @@ -7,15 +7,13 @@ import React, { useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { MANAGEMENT_APP_ID } from '../../../../common/constants'; -import { APP_ID, SecurityPageName } from '../../../../../../common/constants'; +import { APP_ID } from '../../../../../../common/constants'; import { pagePathGetters } from '../../../../../../../fleet/public'; import { getEndpointDetailsPath } from '../../../../common/routing'; import { HostMetadata, MaybeImmutable } from '../../../../../../common/endpoint/types'; -import { useFormatUrl } from '../../../../../common/components/link_to'; import { useEndpointSelector } from './hooks'; import { agentPolicies, uiQueryParams } from '../../store/selectors'; -import { useKibana } from '../../../../../common/lib/kibana'; +import { useAppUrl } from '../../../../../common/lib/kibana/hooks'; import { ContextMenuItemNavByRouterProps } from '../components/context_menu_item_nav_by_rotuer'; import { isEndpointHostIsolated } from '../../../../../common/utils/validators'; import { useLicense } from '../../../../../common/hooks/use_license'; @@ -28,14 +26,9 @@ export const useEndpointActionItems = ( endpointMetadata: MaybeImmutable<HostMetadata> | undefined ): ContextMenuItemNavByRouterProps[] => { const isPlatinumPlus = useLicense().isPlatinumPlus(); - const { formatUrl } = useFormatUrl(SecurityPageName.administration); + const { getAppUrl } = useAppUrl(); const fleetAgentPolicies = useEndpointSelector(agentPolicies); const allCurrentUrlParams = useEndpointSelector(uiQueryParams); - const { - services: { - application: { getUrlForApp }, - }, - } = useKibana(); return useMemo<ContextMenuItemNavByRouterProps[]>(() => { if (endpointMetadata) { @@ -68,11 +61,11 @@ export const useEndpointActionItems = ( 'data-test-subj': 'unIsolateLink', icon: 'logoSecurity', key: 'unIsolateHost', - navigateAppId: MANAGEMENT_APP_ID, + navigateAppId: APP_ID, navigateOptions: { path: endpointUnIsolatePath, }, - href: formatUrl(endpointUnIsolatePath), + href: getAppUrl({ path: endpointUnIsolatePath }), children: ( <FormattedMessage id="xpack.securitySolution.endpoint.actions.unIsolateHost" @@ -86,11 +79,11 @@ export const useEndpointActionItems = ( 'data-test-subj': 'isolateLink', icon: 'logoSecurity', key: 'isolateHost', - navigateAppId: MANAGEMENT_APP_ID, + navigateAppId: APP_ID, navigateOptions: { path: endpointIsolatePath, }, - href: formatUrl(endpointIsolatePath), + href: getAppUrl({ path: endpointIsolatePath }), children: ( <FormattedMessage id="xpack.securitySolution.endpoint.actions.isolateHost" @@ -107,8 +100,8 @@ export const useEndpointActionItems = ( icon: 'logoSecurity', key: 'hostDetailsLink', navigateAppId: APP_ID, - navigateOptions: { path: `hosts/${endpointHostName}` }, - href: `${getUrlForApp('securitySolution')}/hosts/${endpointHostName}`, + navigateOptions: { path: `/hosts/${endpointHostName}` }, + href: getAppUrl({ path: `/hosts/${endpointHostName}` }), children: ( <FormattedMessage id="xpack.securitySolution.endpoint.actions.hostDetails" @@ -128,7 +121,7 @@ export const useEndpointActionItems = ( })[1] }`, }, - href: `${getUrlForApp('fleet')}#${ + href: `${getAppUrl({ appId: 'fleet' })}#${ pagePathGetters.policy_details({ policyId: fleetAgentPolicies[endpointPolicyId], })[1] @@ -153,7 +146,7 @@ export const useEndpointActionItems = ( })[1] }`, }, - href: `${getUrlForApp('fleet')}#${ + href: `${getAppUrl({ appId: 'fleet' })}#${ pagePathGetters.agent_details({ agentId: fleetAgentId, })[1] @@ -177,7 +170,7 @@ export const useEndpointActionItems = ( })[1] }/activity?openReassignFlyout=true`, }, - href: `${getUrlForApp('fleet')}#${ + href: `${getAppUrl({ appId: 'fleet' })}#${ pagePathGetters.agent_details({ agentId: fleetAgentId, })[1] @@ -193,12 +186,5 @@ export const useEndpointActionItems = ( } return []; - }, [ - allCurrentUrlParams, - endpointMetadata, - fleetAgentPolicies, - formatUrl, - getUrlForApp, - isPlatinumPlus, - ]); + }, [allCurrentUrlParams, endpointMetadata, fleetAgentPolicies, getAppUrl, isPlatinumPlus]); }; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 4869ce84fad2c..7bfd77e7dd975 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -42,6 +42,7 @@ import { import { getCurrentIsolationRequestState } from '../store/selectors'; import { licenseService } from '../../../../common/hooks/use_license'; import { FleetActionGenerator } from '../../../../../common/endpoint/data_generators/fleet_action_generator'; +import { APP_PATH, MANAGEMENT_PATH } from '../../../../../common/constants'; // not sure why this can't be imported from '../../../../common/mock/formatted_relative'; // but sure enough it needs to be inline in this one file @@ -94,7 +95,7 @@ describe('when on the endpoint list page', () => { ({ history, store, coreStart, middlewareSpy } = mockedContext); render = () => mockedContext.render(<EndpointList />); reactTestingLibrary.act(() => { - history.push('/endpoints'); + history.push(`${MANAGEMENT_PATH}/endpoints`); }); // Because `.../common/lib/kibana` was mocked, we need to alter these hooks (which are jest.MockFunctions) @@ -442,7 +443,9 @@ describe('when on the endpoint list page', () => { }); const firstPolicyName = (await renderResult.findAllByTestId('policyNameCellLink'))[0]; expect(firstPolicyName).not.toBeNull(); - expect(firstPolicyName.getAttribute('href')).toContain(`policy/${firstPolicyID}`); + expect(firstPolicyName.getAttribute('href')).toEqual( + `${APP_PATH}${MANAGEMENT_PATH}/policy/${firstPolicyID}` + ); }); describe('when the user clicks the first hostname in the table', () => { @@ -657,7 +660,7 @@ describe('when on the endpoint list page', () => { mockEndpointListApi(); reactTestingLibrary.act(() => { - history.push('/endpoints?selected_endpoint=1'); + history.push(`${MANAGEMENT_PATH}/endpoints?selected_endpoint=1`); }); renderAndWaitForData = async () => { @@ -682,7 +685,7 @@ describe('when on the endpoint list page', () => { const policyDetailsLink = await renderResult.findByTestId('policyDetailsValue'); expect(policyDetailsLink).not.toBeNull(); expect(policyDetailsLink.getAttribute('href')).toEqual( - `/policy/${hostDetails.metadata.Endpoint.policy.applied.id}` + `${APP_PATH}${MANAGEMENT_PATH}/policy/${hostDetails.metadata.Endpoint.policy.applied.id}` ); }); @@ -704,7 +707,7 @@ describe('when on the endpoint list page', () => { }); const changedUrlAction = await userChangedUrlChecker; expect(changedUrlAction.payload.pathname).toEqual( - `/policy/${hostDetails.metadata.Endpoint.policy.applied.id}` + `${MANAGEMENT_PATH}/policy/${hostDetails.metadata.Endpoint.policy.applied.id}` ); }); @@ -713,7 +716,7 @@ describe('when on the endpoint list page', () => { const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); expect(policyStatusLink).not.toBeNull(); expect(policyStatusLink.getAttribute('href')).toEqual( - '/endpoints?page_index=0&page_size=10&selected_endpoint=1&show=policy_response' + `${APP_PATH}${MANAGEMENT_PATH}/endpoints?page_index=0&page_size=10&selected_endpoint=1&show=policy_response` ); }); @@ -1003,8 +1006,8 @@ describe('when on the endpoint list page', () => { it('should include the back to details link', async () => { const subHeaderBackLink = await renderResult.findByTestId('flyoutSubHeaderBackButton'); expect(subHeaderBackLink.textContent).toBe('Endpoint Details'); - expect(subHeaderBackLink.getAttribute('href')).toBe( - '/endpoints?page_index=0&page_size=10&selected_endpoint=1' + expect(subHeaderBackLink.getAttribute('href')).toEqual( + `${APP_PATH}${MANAGEMENT_PATH}/endpoints?page_index=0&page_size=10&selected_endpoint=1` ); }); @@ -1055,7 +1058,7 @@ describe('when on the endpoint list page', () => { beforeEach(async () => { getKibanaServicesMock.mockReturnValue(coreStart); reactTestingLibrary.act(() => { - history.push('/endpoints?selected_endpoint=1&show=isolate'); + history.push(`${MANAGEMENT_PATH}/endpoints?selected_endpoint=1&show=isolate`); }); renderResult = await renderAndWaitForData(); // Need to reset `http.post` and adjust it so that the mock for http host @@ -1073,12 +1076,12 @@ describe('when on the endpoint list page', () => { const backButtonLink = renderResult.getByTestId('flyoutSubHeaderBackButton'); expect(backButtonLink.getAttribute('href')).toEqual( - getEndpointDetailsPath({ + `${APP_PATH}${getEndpointDetailsPath({ name: 'endpointDetails', page_index: '0', page_size: '10', selected_endpoint: '1', - }) + })}` ); const changeUrlAction = middlewareSpy.waitForAction('userChangedUrl'); @@ -1088,7 +1091,7 @@ describe('when on the endpoint list page', () => { }); expect((await changeUrlAction).payload).toMatchObject({ - pathname: '/endpoints', + pathname: `${MANAGEMENT_PATH}/endpoints`, search: '?page_index=0&page_size=10&selected_endpoint=1', }); }); @@ -1101,7 +1104,7 @@ describe('when on the endpoint list page', () => { }); expect((await changeUrlAction).payload).toMatchObject({ - pathname: '/endpoints', + pathname: `${MANAGEMENT_PATH}/endpoints`, search: '?page_index=0&page_size=10&selected_endpoint=1', }); }); @@ -1121,7 +1124,7 @@ describe('when on the endpoint list page', () => { }); expect((await changeUrlAction).payload).toMatchObject({ - pathname: '/endpoints', + pathname: `${MANAGEMENT_PATH}/endpoints`, search: '?page_index=0&page_size=10&selected_endpoint=1', }); }); @@ -1149,7 +1152,7 @@ describe('when on the endpoint list page', () => { }); expect((await changeUrlAction).payload).toMatchObject({ - pathname: '/endpoints', + pathname: `${MANAGEMENT_PATH}/endpoints`, search: '?page_index=0&page_size=10', }); @@ -1207,17 +1210,7 @@ describe('when on the endpoint list page', () => { mockEndpointListApi(); reactTestingLibrary.act(() => { - history.push('/endpoints'); - }); - - coreStart.application.getUrlForApp.mockImplementation((appName) => { - switch (appName) { - case 'securitySolution': - return '/app/security'; - case 'fleet': - return '/app/fleet'; - } - return appName; + history.push(`${MANAGEMENT_PATH}/endpoints`); }); renderResult = render(); @@ -1238,19 +1231,19 @@ describe('when on the endpoint list page', () => { it('navigates to the Host Details Isolate flyout', async () => { const isolateLink = await renderResult.findByTestId('isolateLink'); expect(isolateLink.getAttribute('href')).toEqual( - getEndpointDetailsPath({ + `${APP_PATH}${getEndpointDetailsPath({ name: 'endpointIsolate', page_index: '0', page_size: '10', selected_endpoint: hostInfo.metadata.agent.id, - }) + })}` ); }); it('navigates to the Security Solution Host Details page', async () => { const hostLink = await renderResult.findByTestId('hostLink'); expect(hostLink.getAttribute('href')).toEqual( - `/app/security/hosts/${hostInfo.metadata.host.hostname}` + `${APP_PATH}/hosts/${hostInfo.metadata.host.hostname}` ); }); it('navigates to the Ingest Agent Policy page', async () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index 9316d2539d133..7a553cfa8a32a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -47,6 +47,7 @@ import { import { SecurityPageName } from '../../../../app/types'; import { getEndpointListPath, getEndpointDetailsPath } from '../../../common/routing'; import { useFormatUrl } from '../../../../common/components/link_to'; +import { useAppUrl } from '../../../../common/lib/kibana/hooks'; import { EndpointAction } from '../store/action'; import { EndpointPolicyLink } from './components/endpoint_policy_link'; import { OutOfDate } from './components/out_of_date'; @@ -121,7 +122,8 @@ export const EndpointList = () => { endpointsTotalError, isTransformEnabled, } = useEndpointSelector(selector); - const { formatUrl, search } = useFormatUrl(SecurityPageName.administration); + const { search } = useFormatUrl(SecurityPageName.administration); + const { getAppUrl } = useAppUrl(); const dispatch = useDispatch<(a: EndpointAction) => void>(); // cap ability to page at 10k records. (max_result_window) const maxPageCount = totalItemCount > MAX_PAGINATED_ITEM ? MAX_PAGINATED_ITEM : totalItemCount; @@ -160,13 +162,17 @@ export const EndpointList = () => { }/add-integration`, state: { onCancelNavigateTo: [ - 'securitySolution:administration', - { path: getEndpointListPath({ name: 'endpointList' }) }, + 'securitySolution', + { + path: getEndpointListPath({ name: 'endpointList' }), + }, ], - onCancelUrl: formatUrl(getEndpointListPath({ name: 'endpointList' })), + onCancelUrl: getAppUrl({ path: getEndpointListPath({ name: 'endpointList' }) }), onSaveNavigateTo: [ - 'securitySolution:administration', - { path: getEndpointListPath({ name: 'endpointList' }) }, + 'securitySolution', + { + path: getEndpointListPath({ name: 'endpointList' }), + }, ], }, } @@ -201,7 +207,7 @@ export const EndpointList = () => { path: `#/policies/${selectedPolicyId}?openEnrollmentFlyout=true`, state: { onDoneNavigateTo: [ - 'securitySolution:administration', + 'securitySolution', { path: getEndpointListPath({ name: 'endpointList' }) }, ], }, @@ -257,7 +263,7 @@ export const EndpointList = () => { }, search ); - const toRouteUrl = formatUrl(toRoutePath); + const toRouteUrl = getAppUrl({ path: toRoutePath }); return ( <EuiToolTip content={hostname} anchorClassName="eui-textTruncate"> <EndpointListNavLink @@ -336,7 +342,7 @@ export const EndpointList = () => { ...queryParams, selected_endpoint: item.metadata.agent.id, }); - const toRouteUrl = formatUrl(toRoutePath); + const toRouteUrl = getAppUrl({ path: toRoutePath }); return ( <EuiBadge color={POLICY_STATUS_TO_BADGE_COLOR[policy.status]} @@ -416,7 +422,7 @@ export const EndpointList = () => { ], }, ]; - }, [queryParams, search, formatUrl, PAD_LEFT]); + }, [queryParams, search, getAppUrl, PAD_LEFT]); const renderTableOrEmptyState = useMemo(() => { if (endpointsExist || areEndpointsEnrolling) { diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.test.ts index 530a18cd8a312..c7da244c49634 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/middleware.test.ts @@ -81,7 +81,7 @@ describe('Event filters middleware', () => { store.dispatch({ type: 'userChangedUrl', payload: { - pathname: '/event_filters', + pathname: '/administration/event_filters', search: searchParams, hash: '', key: 'ylsd7h', diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.test.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.test.ts index 2bfc6b4378839..1b856f54d16a9 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/store/reducer.test.ts @@ -128,7 +128,7 @@ describe('event filters reducer', () => { describe('UserChangedUrl', () => { const userChangedUrlAction = ( search: string = '', - pathname = '/event_filters' + pathname = '/administration/event_filters' ): UserChangedUrl => ({ type: 'userChangedUrl', payload: { search, pathname, hash: '' }, diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/event_filter_delete_modal.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/event_filter_delete_modal.test.tsx index c594aaa5c7e19..ed18c084c2a05 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/event_filter_delete_modal.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/event_filter_delete_modal.test.tsx @@ -47,7 +47,7 @@ describe('When event filters delete modal is shown', () => { renderResult = mockedContext.render(<EventFilterDeleteModal />); await act(async () => { - history.push('/event_filters'); + history.push('/administration/event_filters'); await waitForAction('userChangedUrl'); diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx index 59d409874c561..d44ce7a136fdf 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx @@ -41,7 +41,7 @@ describe('When on the Event Filters List Page', () => { waitForAction = mockedContext.middlewareSpy.waitForAction; act(() => { - history.push('/event_filters'); + history.push('/administration/event_filters'); }); }); @@ -156,7 +156,7 @@ describe('When on the Event Filters List Page', () => { describe('And search is dispatched', () => { beforeEach(async () => { act(() => { - history.push('/event_filters?filter=test'); + history.push('/administration/event_filters?filter=test'); }); renderResult = render(); await act(async () => { @@ -180,7 +180,7 @@ describe('When on the Event Filters List Page', () => { beforeEach(async () => { renderResult = render(); act(() => { - history.push('/event_filters', { + history.push('/administration/event_filters', { onBackButtonNavigateTo: [{ appId: 'appId' }], backButtonLabel: 'back to fleet', backButtonUrl: '/fleet', @@ -196,7 +196,7 @@ describe('When on the Event Filters List Page', () => { it('back button is not present', () => { act(() => { - history.push('/event_filters'); + history.push('/administration/event_filters'); }); expect(renderResult.queryByTestId('backToOrigin')).toBeNull(); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/index.test.tsx index 68c25e55dc1c9..9f2ed3618b06d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/index.test.tsx @@ -16,21 +16,11 @@ jest.mock('../../common/hooks/endpoint/ingest_enabled'); describe('when in the Admistration tab', () => { let render: () => ReturnType<AppContextTestRender['render']>; - let coreStart: AppContextTestRender['coreStart']; beforeEach(() => { const mockedContext = createAppRootMockRenderer(); - coreStart = mockedContext.coreStart; render = () => mockedContext.render(<ManagementContainer />); - coreStart.http.get.mockImplementation(() => - Promise.resolve({ - response: [ - { - name: 'endpoint', - }, - ], - }) - ); + mockedContext.history.push('/administration/endpoints'); }); it('should display the No Permissions view when Ingest is OFF', async () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/index.tsx b/x-pack/plugins/security_solution/public/management/pages/index.tsx index 8273f1a6e55c2..b3bee78161f39 100644 --- a/x-pack/plugins/security_solution/public/management/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/index.tsx @@ -6,25 +6,24 @@ */ import React, { memo } from 'react'; -import { Route, Switch, useHistory } from 'react-router-dom'; +import { Route, Switch, Redirect } from 'react-router-dom'; import { EuiEmptyPrompt, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { MANAGEMENT_ROUTING_ENDPOINTS_PATH, MANAGEMENT_ROUTING_EVENT_FILTERS_PATH, MANAGEMENT_ROUTING_POLICIES_PATH, - MANAGEMENT_ROUTING_ROOT_PATH, MANAGEMENT_ROUTING_TRUSTED_APPS_PATH, } from '../common/constants'; import { NotFoundPage } from '../../app/404'; import { EndpointsContainer } from './endpoint_hosts'; import { PolicyContainer } from './policy'; import { TrustedAppsContainer } from './trusted_apps'; -import { getEndpointListPath } from '../common/routing'; -import { SecurityPageName } from '../../../common/constants'; +import { MANAGEMENT_PATH, SecurityPageName } from '../../../common/constants'; import { SpyRoute } from '../../common/utils/route/spy_routes'; import { useIngestEnabledCheck } from '../../common/hooks/endpoint/ingest_enabled'; import { EventFiltersContainer } from './event_filters'; +import { getEndpointListPath } from '../common/routing'; const NoPermissions = memo(() => { return ( @@ -56,7 +55,6 @@ const NoPermissions = memo(() => { NoPermissions.displayName = 'NoPermissions'; export const ManagementContainer = memo(() => { - const history = useHistory(); const { allEnabled: isIngestEnabled } = useIngestEnabledCheck(); if (!isIngestEnabled) { @@ -70,14 +68,9 @@ export const ManagementContainer = memo(() => { <Route path={MANAGEMENT_ROUTING_TRUSTED_APPS_PATH} component={TrustedAppsContainer} /> <Route path={MANAGEMENT_ROUTING_EVENT_FILTERS_PATH} component={EventFiltersContainer} /> - <Route - path={MANAGEMENT_ROUTING_ROOT_PATH} - exact - render={() => { - history.replace(getEndpointListPath({ name: 'endpointList' })); - return null; - }} - /> + <Route path={MANAGEMENT_PATH} exact> + <Redirect to={getEndpointListPath({ name: 'endpointList' })} /> + </Route> <Route path="*" component={NotFoundPage} /> </Switch> ); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_event_filters_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_event_filters_card.tsx index 5588cdbe81e3e..22e1c3a612eb7 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_event_filters_card.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_event_filters_card.tsx @@ -6,7 +6,6 @@ */ import React, { memo, useMemo, useState, useEffect, useRef } from 'react'; -import { ApplicationStart, CoreStart } from 'kibana/public'; import { EuiPanel, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -14,27 +13,24 @@ import { PackageCustomExtensionComponentProps, pagePathGetters, } from '../../../../../../../../../fleet/public'; -import { useKibana } from '../../../../../../../../../../../src/plugins/kibana_react/public'; import { getEventFiltersListPath } from '../../../../../../common/routing'; import { GetExceptionSummaryResponse, ListPageRouteState, } from '../../../../../../../../common/endpoint/types'; import { INTEGRATIONS_PLUGIN_ID } from '../../../../../../../../../fleet/common'; -import { MANAGEMENT_APP_ID } from '../../../../../../common/constants'; -import { useToasts } from '../../../../../../../common/lib/kibana'; +import { useKibana, useToasts } from '../../../../../../../common/lib/kibana'; +import { useAppUrl } from '../../../../../../../common/lib/kibana/hooks'; import { LinkWithIcon } from './link_with_icon'; import { ExceptionItemsSummary } from './exception_items_summary'; import { EventFiltersHttpService } from '../../../../../event_filters/service'; import { StyledEuiFlexGridGroup, StyledEuiFlexGridItem } from './styled_components'; export const FleetEventFiltersCard = memo<PackageCustomExtensionComponentProps>(({ pkgkey }) => { + const { getAppUrl } = useAppUrl(); const { - services: { - application: { getUrlForApp }, - http, - }, - } = useKibana<CoreStart & { application: ApplicationStart }>(); + services: { http }, + } = useKibana(); const toasts = useToasts(); const [stats, setStats] = useState<GetExceptionSummaryResponse | undefined>(); const eventFiltersListUrlPath = getEventFiltersListPath(); @@ -82,11 +78,9 @@ export const FleetEventFiltersCard = memo<PackageCustomExtensionComponentProps>( path: fleetPackageCustomUrlPath, }, ], - backButtonUrl: getUrlForApp(INTEGRATIONS_PLUGIN_ID, { - path: fleetPackageCustomUrlPath, - }), + backButtonUrl: getAppUrl({ appId: INTEGRATIONS_PLUGIN_ID, path: fleetPackageCustomUrlPath }), }; - }, [getUrlForApp, pkgkey]); + }, [getAppUrl, pkgkey]); return ( <EuiPanel paddingSize="l"> @@ -107,8 +101,9 @@ export const FleetEventFiltersCard = memo<PackageCustomExtensionComponentProps>( <StyledEuiFlexGridItem gridarea="link" alignitems="flex-end"> <> <LinkWithIcon - appId={MANAGEMENT_APP_ID} - href={getUrlForApp(MANAGEMENT_APP_ID, { path: eventFiltersListUrlPath })} + href={getAppUrl({ + path: eventFiltersListUrlPath, + })} appPath={eventFiltersListUrlPath} appState={eventFiltersRouteState} data-test-subj="linkToEventFilters" diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx index f1c9cb13a27dc..4f10eceb6781c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_package_custom_extension/components/fleet_trusted_apps_card.tsx @@ -6,7 +6,6 @@ */ import React, { memo, useMemo, useState, useEffect, useRef } from 'react'; -import { ApplicationStart, CoreStart } from 'kibana/public'; import { EuiPanel, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -14,27 +13,25 @@ import { PackageCustomExtensionComponentProps, pagePathGetters, } from '../../../../../../../../../fleet/public'; -import { useKibana } from '../../../../../../../../../../../src/plugins/kibana_react/public'; import { getTrustedAppsListPath } from '../../../../../../common/routing'; import { ListPageRouteState, GetExceptionSummaryResponse, } from '../../../../../../../../common/endpoint/types'; import { INTEGRATIONS_PLUGIN_ID } from '../../../../../../../../../fleet/common'; -import { MANAGEMENT_APP_ID } from '../../../../../../common/constants'; -import { useToasts } from '../../../../../../../common/lib/kibana'; + +import { useAppUrl } from '../../../../../../../common/lib/kibana/hooks'; +import { useKibana, useToasts } from '../../../../../../../common/lib/kibana'; import { LinkWithIcon } from './link_with_icon'; import { ExceptionItemsSummary } from './exception_items_summary'; import { TrustedAppsHttpService } from '../../../../../trusted_apps/service'; import { StyledEuiFlexGridGroup, StyledEuiFlexGridItem } from './styled_components'; export const FleetTrustedAppsCard = memo<PackageCustomExtensionComponentProps>(({ pkgkey }) => { + const { getAppUrl } = useAppUrl(); const { - services: { - application: { getUrlForApp }, - http, - }, - } = useKibana<CoreStart & { application: ApplicationStart }>(); + services: { http }, + } = useKibana(); const toasts = useToasts(); const [stats, setStats] = useState<GetExceptionSummaryResponse | undefined>(); const trustedAppsApi = useMemo(() => new TrustedAppsHttpService(http), [http]); @@ -83,11 +80,9 @@ export const FleetTrustedAppsCard = memo<PackageCustomExtensionComponentProps>(( path: fleetPackageCustomUrlPath, }, ], - backButtonUrl: getUrlForApp(INTEGRATIONS_PLUGIN_ID, { - path: fleetPackageCustomUrlPath, - }), + backButtonUrl: getAppUrl({ appId: INTEGRATIONS_PLUGIN_ID, path: fleetPackageCustomUrlPath }), }; - }, [getUrlForApp, pkgkey]); + }, [getAppUrl, pkgkey]); return ( <EuiPanel paddingSize="l"> <StyledEuiFlexGridGroup alignItems="baseline" justifyContent="center"> @@ -107,8 +102,9 @@ export const FleetTrustedAppsCard = memo<PackageCustomExtensionComponentProps>(( <StyledEuiFlexGridItem gridarea="link" alignitems="flex-end"> <> <LinkWithIcon - appId={MANAGEMENT_APP_ID} - href={getUrlForApp(MANAGEMENT_APP_ID, { path: trustedAppsListUrlPath })} + href={getAppUrl({ + path: trustedAppsListUrlPath, + })} appPath={trustedAppsListUrlPath} appState={trustedAppRouteState} data-test-subj="linkToTrustedApps" diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx index 1766048a3985a..93cf0f370a715 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx @@ -16,7 +16,6 @@ import { getPolicyDetailPath, getEndpointListPath } from '../../../common/routin import { policyListApiPathHandlers } from '../store/test_mock_utils'; import { licenseService } from '../../../../common/hooks/use_license'; -jest.mock('../../../../common/components/link_to'); jest.mock('../../../../common/hooks/use_license'); describe('Policy Details', () => { @@ -130,7 +129,7 @@ describe('Policy Details', () => { const backToListLink = policyView.find('LinkIcon[dataTestSubj="policyDetailsBackLink"]'); expect(backToListLink.prop('iconType')).toBe('arrowLeft'); - expect(backToListLink.prop('href')).toBe(endpointListPath); + expect(backToListLink.prop('href')).toBe(`/app/security${endpointListPath}`); expect(backToListLink.text()).toBe('Back to endpoint hosts'); const pageTitle = policyView.find('h1[data-test-subj="header-page-title"]'); @@ -172,7 +171,7 @@ describe('Policy Details', () => { cancelbutton.simulate('click', { button: 0 }); const navigateToAppMockedCalls = coreStart.application.navigateToApp.mock.calls; expect(navigateToAppMockedCalls[navigateToAppMockedCalls.length - 1]).toEqual([ - 'securitySolution:administration', + 'securitySolution', { path: endpointListPath }, ]); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx index e9cdd16554f33..b31ec47fdfc49 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx @@ -38,9 +38,8 @@ import { AppAction } from '../../../../common/store/actions'; import { SpyRoute } from '../../../../common/utils/route/spy_routes'; import { SecurityPageName } from '../../../../app/types'; import { getEndpointListPath } from '../../../common/routing'; -import { useFormatUrl } from '../../../../common/components/link_to'; import { useNavigateToAppEventHandler } from '../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; -import { MANAGEMENT_APP_ID } from '../../../common/constants'; +import { APP_ID } from '../../../../../common/constants'; import { PolicyDetailsRouteState } from '../../../../../common/endpoint/types'; import { SecuritySolutionPageWrapper } from '../../../../common/components/page_wrapper'; import { HeaderPage } from '../../../../common/components/header_page'; @@ -73,7 +72,6 @@ export const PolicyDetails = React.memo(() => { }, } = useKibana<{ application: ApplicationStart }>(); const toasts = useToasts(); - const { formatUrl } = useFormatUrl(SecurityPageName.administration); const { state: locationRouteState } = useLocation<PolicyDetailsRouteState>(); // Store values @@ -128,7 +126,7 @@ export const PolicyDetails = React.memo(() => { const routingOnCancelNavigateTo = routeState?.onCancelNavigateTo; const navigateToAppArguments = useMemo((): Parameters<ApplicationStart['navigateToApp']> => { - return routingOnCancelNavigateTo ?? [MANAGEMENT_APP_ID, { path: hostListRouterPath }]; + return routingOnCancelNavigateTo ?? [APP_ID, { path: hostListRouterPath }]; }, [hostListRouterPath, routingOnCancelNavigateTo]); const handleCancelOnClick = useNavigateToAppEventHandler(...navigateToAppArguments); @@ -208,8 +206,7 @@ export const PolicyDetails = React.memo(() => { defaultMessage: 'Back to endpoint hosts', } ), - href: formatUrl(hostListRouterPath), - pageId: SecurityPageName.administration, + pageId: SecurityPageName.endpoints, dataTestSubj: 'policyDetailsBackLink', }} > diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx index c17d6df36be68..6374ba3bc4f5f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/malware.tsx @@ -46,7 +46,7 @@ export const MalwareProtections = React.memo(() => { defaultMessage="View {detectionRulesLink}. Prebuilt rules are tagged “Elastic” on the Detection Rules page." values={{ detectionRulesLink: ( - <LinkToApp appId={`${APP_ID}:${SecurityPageName.detections}`} appPath={`/rules`}> + <LinkToApp appId={APP_ID} deepLinkId={SecurityPageName.rules}> <FormattedMessage id="xpack.securitySolution.endpoint.policy.details.detectionRulesLink" defaultMessage="related detection rules" diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/ransomware.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/ransomware.tsx index 60d20665a6827..70f41015bc257 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/ransomware.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/ransomware.tsx @@ -44,7 +44,7 @@ export const Ransomware = React.memo(() => { defaultMessage="View {detectionRulesLink}. Prebuilt rules are tagged “Elastic” on the Detection Rules page." values={{ detectionRulesLink: ( - <LinkToApp appId={`${APP_ID}:${SecurityPageName.detections}`} appPath={`/rules`}> + <LinkToApp appId={APP_ID} deepLinkId={SecurityPageName.rules}> <FormattedMessage id="xpack.securitySolution.endpoint.policy.details.detectionRulesLink" defaultMessage="related detection rules" diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts index ed45d077dd0ca..9624987c8af56 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts @@ -124,7 +124,9 @@ describe('middleware', () => { service.getTrustedAppsList.mockResolvedValue(createGetTrustedListAppsResponse(pagination)); - store.dispatch(createUserChangedUrlAction('/trusted_apps', '?page_index=2&page_size=50')); + store.dispatch( + createUserChangedUrlAction('/administration/trusted_apps', '?page_index=2&page_size=50') + ); expect(store.getState()).toStrictEqual({ ...initialState, @@ -161,11 +163,15 @@ describe('middleware', () => { service.getTrustedAppsList.mockResolvedValue(createGetTrustedListAppsResponse(pagination)); - store.dispatch(createUserChangedUrlAction('/trusted_apps', '?page_index=2&page_size=50')); + store.dispatch( + createUserChangedUrlAction('/administration/trusted_apps', '?page_index=2&page_size=50') + ); await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); - store.dispatch(createUserChangedUrlAction('/trusted_apps', '?page_index=2&page_size=50')); + store.dispatch( + createUserChangedUrlAction('/administration/trusted_apps', '?page_index=2&page_size=50') + ); expect(service.getTrustedAppsList).toBeCalledTimes(2); expect(store.getState()).toStrictEqual({ @@ -186,7 +192,7 @@ describe('middleware', () => { service.getTrustedAppsList.mockResolvedValue(createGetTrustedListAppsResponse(pagination)); - store.dispatch(createUserChangedUrlAction('/trusted_apps')); + store.dispatch(createUserChangedUrlAction('/administration/trusted_apps')); await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); @@ -227,7 +233,9 @@ describe('middleware', () => { body: createServerApiError('Internal Server Error'), }); - store.dispatch(createUserChangedUrlAction('/trusted_apps', '?page_index=2&page_size=50')); + store.dispatch( + createUserChangedUrlAction('/administration/trusted_apps', '?page_index=2&page_size=50') + ); await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); @@ -281,7 +289,7 @@ describe('middleware', () => { service.getTrustedAppsList.mockResolvedValue(getTrustedAppsListResponse); service.deleteTrustedApp.mockResolvedValue(); - store.dispatch(createUserChangedUrlAction('/trusted_apps')); + store.dispatch(createUserChangedUrlAction('/administration/trusted_apps')); await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); @@ -300,7 +308,7 @@ describe('middleware', () => { service.getTrustedAppsList.mockResolvedValue(getTrustedAppsListResponse); service.deleteTrustedApp.mockResolvedValue(); - store.dispatch(createUserChangedUrlAction('/trusted_apps')); + store.dispatch(createUserChangedUrlAction('/administration/trusted_apps')); await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); @@ -340,7 +348,7 @@ describe('middleware', () => { service.getTrustedAppsList.mockResolvedValue(getTrustedAppsListResponse); service.deleteTrustedApp.mockResolvedValue(); - store.dispatch(createUserChangedUrlAction('/trusted_apps')); + store.dispatch(createUserChangedUrlAction('/administration/trusted_apps')); await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); @@ -381,7 +389,7 @@ describe('middleware', () => { service.getTrustedAppsList.mockResolvedValue(getTrustedAppsListResponse); service.deleteTrustedApp.mockRejectedValue({ body: notFoundError }); - store.dispatch(createUserChangedUrlAction('/trusted_apps')); + store.dispatch(createUserChangedUrlAction('/administration/trusted_apps')); await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts index 42659e5cc3498..ac4d29a6016b2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts @@ -30,7 +30,7 @@ describe('reducer', () => { const result = trustedAppsPageReducer( initialState, createUserChangedUrlAction( - '/trusted_apps', + '/administration/trusted_apps', '?page_index=5&page_size=50&show=create&view_type=list&filter=test' ) ); @@ -55,7 +55,10 @@ describe('reducer', () => { ...initialState, location: { page_index: 5, page_size: 50, view_type: 'grid', filter: '' }, }, - createUserChangedUrlAction('/trusted_apps', '?page_index=b&page_size=60&show=a&view_type=c') + createUserChangedUrlAction( + '/administration/trusted_apps', + '?page_index=b&page_size=60&show=a&view_type=c' + ) ); expect(result).toStrictEqual({ ...initialState, active: true }); @@ -67,7 +70,7 @@ describe('reducer', () => { ...initialState, location: { page_index: 5, page_size: 50, view_type: 'grid', filter: '' }, }, - createUserChangedUrlAction('/trusted_apps') + createUserChangedUrlAction('/administration/trusted_apps') ); expect(result).toStrictEqual({ ...initialState, active: true }); @@ -76,7 +79,7 @@ describe('reducer', () => { it('makes page state inactive and resets list to uninitialised state when navigating away', () => { const result = trustedAppsPageReducer( { ...initialState, listView: createLoadedListViewWithPagination(initialNow), active: true }, - createUserChangedUrlAction('/endpoints') + createUserChangedUrlAction('/administration/endpoints') ); expect(result).toStrictEqual(initialState); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.tsx index 7ec8d311a9156..99db45c0e4b84 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.tsx @@ -21,10 +21,8 @@ import { EuiSelectableOption } from '@elastic/eui/src/components/selectable/sele import { FormattedMessage } from '@kbn/i18n/react'; import styled from 'styled-components'; import { PolicyData } from '../../../../../../../common/endpoint/types'; -import { MANAGEMENT_APP_ID } from '../../../../../common/constants'; import { getPolicyDetailPath } from '../../../../../common/routing'; -import { useFormatUrl } from '../../../../../../common/components/link_to'; -import { SecurityPageName } from '../../../../../../../common/constants'; +import { useAppUrl } from '../../../../../../common/lib/kibana/hooks'; import { LinkToApp } from '../../../../../../common/components/endpoint/link_to_app'; import { useTestIdGenerator } from '../../../../../components/hooks/use_test_id_generator'; @@ -68,7 +66,7 @@ export const EffectedPolicySelect = memo<EffectedPolicySelectProps>( 'data-test-subj': dataTestSubj, ...otherSelectableProps }) => { - const { formatUrl } = useFormatUrl(SecurityPageName.administration); + const { getAppUrl } = useAppUrl(); const getTestId = useTestIdGenerator(dataTestSubj); @@ -89,8 +87,7 @@ export const EffectedPolicySelect = memo<EffectedPolicySelectProps>( ), append: ( <LinkToApp - href={formatUrl(getPolicyDetailPath(policy.id))} - appId={MANAGEMENT_APP_ID} + href={getAppUrl({ path: getPolicyDetailPath(policy.id) })} appPath={getPolicyDetailPath(policy.id)} target="_blank" > @@ -106,7 +103,7 @@ export const EffectedPolicySelect = memo<EffectedPolicySelectProps>( 'data-test-subj': `policy-${policy.id}`, })) .sort(({ label: labelA }, { label: labelB }) => labelA.localeCompare(labelB)); - }, [formatUrl, isGlobal, options, selected]); + }, [getAppUrl, isGlobal, options, selected]); const handleOnPolicySelectChange = useCallback< Required<EuiSelectableProps<OptionPolicyData>>['onChange'] diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/index.test.tsx index 4ed9a3c5a0119..74f3f0524b304 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/index.test.tsx @@ -85,7 +85,9 @@ describe('TrustedAppsGrid', () => { createListLoadedResourceState({ pageSize: 10 }, now) ) ); - store.dispatch(createUserChangedUrlAction('/trusted_apps', '?page_index=2&page_size=50')); + store.dispatch( + createUserChangedUrlAction('/administration/trusted_apps', '?page_index=2&page_size=50') + ); expect(renderList(store).container).toMatchSnapshot(); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/index.test.tsx index d054061dbba31..64efda2c90ed1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_list/index.test.tsx @@ -92,7 +92,9 @@ describe('TrustedAppsList', () => { createListLoadedResourceState({ pageSize: 20 }, now) ) ); - store.dispatch(createUserChangedUrlAction('/trusted_apps', '?page_index=2&page_size=50')); + store.dispatch( + createUserChangedUrlAction('/administration/trusted_apps', '?page_index=2&page_size=50') + ); expect(renderList(store).container).toMatchSnapshot(); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx index adc9438f27d74..970ade80bd8db 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx @@ -144,7 +144,7 @@ describe('When on the Trusted Apps Page', () => { waitForAction = mockedContext.middlewareSpy.waitForAction; render = () => mockedContext.render(<TrustedAppsPage />); reactTestingLibrary.act(() => { - history.push('/trusted_apps'); + history.push('/administration/trusted_apps'); }); window.scrollTo = jest.fn(); }); @@ -305,7 +305,7 @@ describe('When on the Trusted Apps Page', () => { }); reactTestingLibrary.act(() => { - history.push('/trusted_apps?show=edit&id=9999-edit-8888'); + history.push('/administration/trusted_apps?show=edit&id=9999-edit-8888'); }); }); @@ -323,7 +323,7 @@ describe('When on the Trusted Apps Page', () => { it('should redirect to list and show toast message if `id` is missing from URL', async () => { reactTestingLibrary.act(() => { - history.push('/trusted_apps?show=edit&id='); + history.push('/administration/trusted_apps?show=edit&id='); }); await renderAndWaitForGetApi(); @@ -367,7 +367,7 @@ describe('When on the Trusted Apps Page', () => { beforeEach(async () => { reactTestingLibrary.act(() => { - history.push('/trusted_apps?view_type=list'); + history.push('/administration/trusted_apps?view_type=list'); }); renderResult = await renderWithListData(); @@ -477,7 +477,7 @@ describe('When on the Trusted Apps Page', () => { it('should preserve other URL search params', async () => { reactTestingLibrary.act(() => { - history.push('/trusted_apps?page_index=2&page_size=20'); + history.push('/administration/trusted_apps?page_index=2&page_size=20'); }); await renderAndClickAddButton(); expect(history.location.search).toBe('?page_index=2&page_size=20&show=create'); @@ -884,7 +884,7 @@ describe('When on the Trusted Apps Page', () => { beforeEach(async () => { mockListApis(coreStart.http); reactTestingLibrary.act(() => { - history.push('/trusted_apps?filter=test'); + history.push('/administration/trusted_apps?filter=test'); }); renderResult = render(); await act(async () => { @@ -912,7 +912,7 @@ describe('When on the Trusted Apps Page', () => { await waitForAction('trustedAppsListResourceStateChanged'); }); reactTestingLibrary.act(() => { - history.push('/trusted_apps', { + history.push('/administration/trusted_apps', { onBackButtonNavigateTo: [{ appId: 'appId' }], backButtonLabel: 'back to fleet', backButtonUrl: '/fleet', @@ -928,7 +928,7 @@ describe('When on the Trusted Apps Page', () => { it('back button is not present', () => { reactTestingLibrary.act(() => { - history.push('/trusted_apps'); + history.push('/administration/trusted_apps'); }); expect(renderResult.queryByTestId('backToOrigin')).toBeNull(); }); diff --git a/x-pack/plugins/security_solution/public/management/routes.tsx b/x-pack/plugins/security_solution/public/management/routes.tsx index b9143a862e736..bbc165d51a46c 100644 --- a/x-pack/plugins/security_solution/public/management/routes.tsx +++ b/x-pack/plugins/security_solution/public/management/routes.tsx @@ -6,19 +6,26 @@ */ import React from 'react'; -import { Route, Switch } from 'react-router-dom'; +import { TrackApplicationView } from '../../../../../src/plugins/usage_collection/public'; +import { MANAGEMENT_PATH, SecurityPageName } from '../../common/constants'; import { ManagementContainer } from './pages'; -import { NotFoundPage } from '../app/404'; +import { SecuritySubPluginRoutes } from '../app/types'; import { CurrentLicense } from '../common/components/current_license'; /** * Returns the React Router Routes for the management area */ -export const ManagementRoutes = () => ( - <CurrentLicense> - <Switch> - <Route path="/" component={ManagementContainer} /> - <Route render={() => <NotFoundPage />} /> - </Switch> - </CurrentLicense> +const ManagementRoutes = () => ( + <TrackApplicationView viewId={SecurityPageName.administration}> + <CurrentLicense> + <ManagementContainer /> + </CurrentLicense> + </TrackApplicationView> ); + +export const routes: SecuritySubPluginRoutes = [ + { + path: MANAGEMENT_PATH, + render: ManagementRoutes, + }, +]; diff --git a/x-pack/plugins/security_solution/public/network/index.ts b/x-pack/plugins/security_solution/public/network/index.ts index 5764484a53987..f34ebcc6e33b9 100644 --- a/x-pack/plugins/security_solution/public/network/index.ts +++ b/x-pack/plugins/security_solution/public/network/index.ts @@ -7,7 +7,7 @@ import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { SecuritySubPluginWithStore } from '../app/types'; -import { NetworkRoutes } from './routes'; +import { routes } from './routes'; import { initialNetworkState, networkReducer, NetworkState } from './store'; import { TimelineId } from '../../common/types/timeline'; import { getTimelinesInStorageByIds } from '../timelines/containers/local_storage'; @@ -17,7 +17,7 @@ export class Network { public start(storage: Storage): SecuritySubPluginWithStore<'network', NetworkState> { return { - SubPluginRoutes: NetworkRoutes, + routes, storageTimelines: { timelineById: getTimelinesInStorageByIds(storage, [TimelineId.networkPageExternalAlerts]), }, diff --git a/x-pack/plugins/security_solution/public/network/pages/details/utils.ts b/x-pack/plugins/security_solution/public/network/pages/details/utils.ts index bfba7fa938a81..637180203c6d1 100644 --- a/x-pack/plugins/security_solution/public/network/pages/details/utils.ts +++ b/x-pack/plugins/security_solution/public/network/pages/details/utils.ts @@ -36,7 +36,8 @@ export const getBreadcrumbs = ( let breadcrumb = [ { text: i18n.PAGE_TITLE, - href: getUrlForApp(`${APP_ID}:${SecurityPageName.network}`, { + href: getUrlForApp(APP_ID, { + deepLinkId: SecurityPageName.network, path: !isEmpty(search[0]) ? search[0] : '', }), }, @@ -46,7 +47,8 @@ export const getBreadcrumbs = ( ...breadcrumb, { text: decodeIpv6(params.detailName), - href: getUrlForApp(`${APP_ID}:${SecurityPageName.network}`, { + href: getUrlForApp(APP_ID, { + deepLinkId: SecurityPageName.network, path: getNetworkDetailsUrl( params.detailName, params.flowTarget, diff --git a/x-pack/plugins/security_solution/public/network/pages/index.tsx b/x-pack/plugins/security_solution/public/network/pages/index.tsx index ddc098823470a..965d461665ec2 100644 --- a/x-pack/plugins/security_solution/public/network/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/index.tsx @@ -6,7 +6,7 @@ */ import React, { useMemo } from 'react'; -import { Route, Switch, RouteComponentProps, useHistory } from 'react-router-dom'; +import { Redirect, Route, Switch } from 'react-router-dom'; import { useMlCapabilities } from '../../common/components/ml/hooks/use_ml_capabilities'; import { hasMlUserPermissions } from '../../../common/machine_learning/has_ml_user_permissions'; @@ -17,14 +17,11 @@ import { getNetworkRoutePath } from './navigation'; import { NetworkRouteType } from './navigation/types'; import { MlNetworkConditionalContainer } from '../../common/components/ml/conditional_links/ml_network_conditional_container'; import { FlowTarget } from '../../../common/search_strategy'; +import { NETWORK_PATH } from '../../../common/constants'; -type Props = Partial<RouteComponentProps<{}>> & { url: string }; +const ipDetailsPageBasePath = `${NETWORK_PATH}/ip/:detailName`; -const networkPagePath = ''; -const ipDetailsPageBasePath = `/ip/:detailName`; - -const NetworkContainerComponent: React.FC<Props> = () => { - const history = useHistory(); +const NetworkContainerComponent = () => { const capabilities = useMlCapabilities(); const capabilitiesFetched = capabilities.capabilitiesFetched; const userHasMlUserPermissions = useMemo(() => hasMlUserPermissions(capabilities), [ @@ -38,14 +35,18 @@ const NetworkContainerComponent: React.FC<Props> = () => { return ( <Switch> <Route - path="/ml-network" - render={({ location, match }) => ( - <MlNetworkConditionalContainer location={location} url={match.url} /> + exact + strict + path={NETWORK_PATH} + render={({ location: { search = '' } }) => ( + <Redirect to={{ pathname: `${NETWORK_PATH}/${NetworkRouteType.flows}`, search }} /> )} /> + <Route path={`${NETWORK_PATH}/ml-network`}> + <MlNetworkConditionalContainer /> + </Route> <Route strict path={networkRoutePath}> <Network - networkPagePath={networkPagePath} capabilitiesFetched={capabilities.capabilitiesFetched} hasMlUserPermissions={userHasMlUserPermissions} /> @@ -60,17 +61,14 @@ const NetworkContainerComponent: React.FC<Props> = () => { match: { params: { detailName }, }, - }) => { - history.replace(`ip/${detailName}/${FlowTarget.source}${search}`); - return null; - }} - /> - <Route - path="/" - render={({ location: { search = '' } }) => { - history.replace(`${NetworkRouteType.flows}${search}`); - return null; - }} + }) => ( + <Redirect + to={{ + pathname: `${NETWORK_PATH}/ip/${detailName}/${FlowTarget.source}`, + search, + }} + /> + )} /> </Switch> ); diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/nav_tabs.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/nav_tabs.tsx index 8e07cba1f5c1a..607b2e02ac961 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/nav_tabs.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/nav_tabs.tsx @@ -8,9 +8,9 @@ import { omit } from 'lodash/fp'; import * as i18n from '../translations'; import { NetworkNavTab, NetworkRouteType } from './types'; -import { SecurityPageName } from '../../../app/types'; +import { NETWORK_PATH } from '../../../../common/constants'; -const getTabsOnNetworkUrl = (tabName: NetworkRouteType) => `/${tabName}`; +const getTabsOnNetworkUrl = (tabName: NetworkRouteType) => `${NETWORK_PATH}/${tabName}`; export const navTabsNetwork = (hasMlUserPermissions: boolean): NetworkNavTab => { const networkNavTabs = { @@ -19,48 +19,36 @@ export const navTabsNetwork = (hasMlUserPermissions: boolean): NetworkNavTab => name: i18n.NAVIGATION_FLOWS_TITLE, href: getTabsOnNetworkUrl(NetworkRouteType.flows), disabled: false, - urlKey: 'network', - pageId: SecurityPageName.network, }, [NetworkRouteType.dns]: { id: NetworkRouteType.dns, name: i18n.NAVIGATION_DNS_TITLE, href: getTabsOnNetworkUrl(NetworkRouteType.dns), disabled: false, - urlKey: 'network', - pageId: SecurityPageName.network, }, [NetworkRouteType.http]: { id: NetworkRouteType.http, name: i18n.NAVIGATION_HTTP_TITLE, href: getTabsOnNetworkUrl(NetworkRouteType.http), disabled: false, - urlKey: 'network', - pageId: SecurityPageName.network, }, [NetworkRouteType.tls]: { id: NetworkRouteType.tls, name: i18n.NAVIGATION_TLS_TITLE, href: getTabsOnNetworkUrl(NetworkRouteType.tls), disabled: false, - urlKey: 'network', - pageId: SecurityPageName.network, }, [NetworkRouteType.anomalies]: { id: NetworkRouteType.anomalies, name: i18n.NAVIGATION_ANOMALIES_TITLE, href: getTabsOnNetworkUrl(NetworkRouteType.anomalies), disabled: false, - urlKey: 'network', - pageId: SecurityPageName.network, }, [NetworkRouteType.alerts]: { id: NetworkRouteType.alerts, name: i18n.NAVIGATION_ALERTS_TITLE, href: getTabsOnNetworkUrl(NetworkRouteType.alerts), disabled: false, - urlKey: 'network', - pageId: SecurityPageName.network, }, }; diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/network_routes.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/network_routes.tsx index a1d012d64d7b7..ea026664ce1e4 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/network_routes.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/network_routes.tsx @@ -24,10 +24,10 @@ import { TlsQueryTabBody } from './tls_query_tab_body'; import { Anomaly } from '../../../common/components/ml/types'; import { NetworkAlertsQueryTabBody } from './alerts_query_tab_body'; import { UpdateDateRange } from '../../../common/components/charts/common'; +import { NETWORK_PATH } from '../../../../common/constants'; export const NetworkRoutes = React.memo<NetworkRoutesProps>( ({ - networkPagePath, docValueFields, type, to, @@ -108,10 +108,10 @@ export const NetworkRoutes = React.memo<NetworkRoutesProps>( return ( <Switch> - <Route path={`/:tabName(${NetworkRouteType.dns})`}> + <Route path={`${NETWORK_PATH}/:tabName(${NetworkRouteType.dns})`}> <DnsQueryTabBody {...tabProps} docValueFields={docValueFields} /> </Route> - <Route path={`/:tabName(${NetworkRouteType.flows})`}> + <Route path={`${NETWORK_PATH}/:tabName(${NetworkRouteType.flows})`}> <> <ConditionalFlexGroup direction="column"> <EuiFlexItem> @@ -137,19 +137,19 @@ export const NetworkRoutes = React.memo<NetworkRoutesProps>( </ConditionalFlexGroup> </> </Route> - <Route path={`/:tabName(${NetworkRouteType.http})`}> + <Route path={`${NETWORK_PATH}/:tabName(${NetworkRouteType.http})`}> <HttpQueryTabBody {...tabProps} /> </Route> - <Route path={`/:tabName(${NetworkRouteType.tls})`}> + <Route path={`${NETWORK_PATH}/:tabName(${NetworkRouteType.tls})`}> <TlsQueryTabBody {...tabProps} flowTarget={FlowTargetSourceDest.source} /> </Route> - <Route path={`/:tabName(${NetworkRouteType.anomalies})`}> + <Route path={`${NETWORK_PATH}/:tabName(${NetworkRouteType.anomalies})`}> <AnomaliesQueryTabBody {...anomaliesProps} AnomaliesTableComponent={AnomaliesNetworkTable} /> </Route> - <Route path={`/:tabName(${NetworkRouteType.alerts})`}> + <Route path={`${NETWORK_PATH}/:tabName(${NetworkRouteType.alerts})`}> <NetworkAlertsQueryTabBody {...tabProps} /> </Route> </Switch> diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts b/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts index ed1682e38da9a..075aa46637a07 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts @@ -47,7 +47,6 @@ export type HttpQueryTabBodyProps = QueryTabBodyProps & { export type NetworkRoutesProps = GlobalTimeArgs & { docValueFields: DocValueFields[]; - networkPagePath: string; type: networkModel.NetworkType; filterQuery?: string | ESTermQuery; indexPattern: IIndexPattern; diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/utils.ts b/x-pack/plugins/security_solution/public/network/pages/navigation/utils.ts index 8f2de2b0c9812..fcd81ca975584 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/utils.ts +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/utils.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { NETWORK_PATH } from '../../../../common/constants'; import { GetNetworkRoutePath, NetworkRouteType } from './types'; export const getNetworkRoutePath: GetNetworkRoutePath = ( @@ -12,11 +13,11 @@ export const getNetworkRoutePath: GetNetworkRoutePath = ( hasMlUserPermission ) => { if (capabilitiesFetched && !hasMlUserPermission) { - return `/:tabName(${NetworkRouteType.flows}|${NetworkRouteType.dns}|${NetworkRouteType.http}|${NetworkRouteType.tls}|${NetworkRouteType.alerts})`; + return `${NETWORK_PATH}/:tabName(${NetworkRouteType.flows}|${NetworkRouteType.dns}|${NetworkRouteType.http}|${NetworkRouteType.tls}|${NetworkRouteType.alerts})`; } return ( - `/:tabName(` + + `${NETWORK_PATH}/:tabName(` + `${NetworkRouteType.flows}|` + `${NetworkRouteType.dns}|` + `${NetworkRouteType.anomalies}|` + diff --git a/x-pack/plugins/security_solution/public/network/pages/network.tsx b/x-pack/plugins/security_solution/public/network/pages/network.tsx index 13c04a5e5ec5b..b08a75215a408 100644 --- a/x-pack/plugins/security_solution/public/network/pages/network.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/network.tsx @@ -62,7 +62,7 @@ const StyledFullHeightContainer = styled.div` `; const NetworkComponent = React.memo<NetworkComponentProps>( - ({ networkPagePath, hasMlUserPermissions, capabilitiesFetched }) => { + ({ hasMlUserPermissions, capabilitiesFetched }) => { const dispatch = useDispatch(); const containerElement = useRef<HTMLDivElement | null>(null); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); @@ -193,9 +193,7 @@ const NetworkComponent = React.memo<NetworkComponentProps>( <> <Display show={!globalFullScreen}> <EuiSpacer /> - <SecuritySolutionTabNavigation navTabs={navTabsNetwork(hasMlUserPermissions)} /> - <EuiSpacer /> </Display> @@ -210,7 +208,6 @@ const NetworkComponent = React.memo<NetworkComponentProps>( setAbsoluteRangeDatePicker={setAbsoluteRangeDatePicker} type={networkModel.NetworkType.page} to={to} - networkPagePath={networkPagePath} /> </> ) : ( diff --git a/x-pack/plugins/security_solution/public/network/pages/types.ts b/x-pack/plugins/security_solution/public/network/pages/types.ts index 1d727a2d219d7..df5ca5656abfb 100644 --- a/x-pack/plugins/security_solution/public/network/pages/types.ts +++ b/x-pack/plugins/security_solution/public/network/pages/types.ts @@ -16,7 +16,6 @@ export type SetAbsoluteRangeDatePicker = ActionCreator<{ }>; export type NetworkComponentProps = Partial<RouteComponentProps<{}>> & { - networkPagePath: string; hasMlUserPermissions: boolean; capabilitiesFetched: boolean; }; diff --git a/x-pack/plugins/security_solution/public/network/routes.tsx b/x-pack/plugins/security_solution/public/network/routes.tsx index 9704c92e19336..32fbe96a21caa 100644 --- a/x-pack/plugins/security_solution/public/network/routes.tsx +++ b/x-pack/plugins/security_solution/public/network/routes.tsx @@ -6,17 +6,21 @@ */ import React from 'react'; -import { Route, Switch } from 'react-router-dom'; - import { NetworkContainer } from './pages'; -import { NotFoundPage } from '../app/404'; + +import { TrackApplicationView } from '../../../../../src/plugins/usage_collection/public'; +import { SecurityPageName, SecuritySubPluginRoutes } from '../app/types'; +import { NETWORK_PATH } from '../../common/constants'; export const NetworkRoutes = () => ( - <Switch> - <Route - path="/" - render={({ location, match }) => <NetworkContainer location={location} url={match.url} />} - /> - <Route render={() => <NotFoundPage />} /> - </Switch> + <TrackApplicationView viewId={SecurityPageName.network}> + <NetworkContainer /> + </TrackApplicationView> ); + +export const routes: SecuritySubPluginRoutes = [ + { + path: NETWORK_PATH, + render: NetworkRoutes, + }, +]; diff --git a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx index 9957d43551ff9..98874c25e0ef8 100644 --- a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx @@ -67,7 +67,8 @@ const AlertsByCategoryComponent: React.FC<Props> = ({ const goToHostAlerts = useCallback( (ev) => { ev.preventDefault(); - navigateToApp(`${APP_ID}:${SecurityPageName.hosts}`, { + navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.hosts, path: getTabsOnHostsUrl(HostsTableType.alerts, urlSearch), }); }, diff --git a/x-pack/plugins/security_solution/public/overview/components/endpoint_notice/index.tsx b/x-pack/plugins/security_solution/public/overview/components/endpoint_notice/index.tsx index e908748d0028c..6a00afde7c599 100644 --- a/x-pack/plugins/security_solution/public/overview/components/endpoint_notice/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/endpoint_notice/index.tsx @@ -8,15 +8,16 @@ import React, { memo } from 'react'; import { EuiCallOut, EuiButton, EuiButtonEmpty } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { useKibana } from '../../../common/lib/kibana'; +import { APP_ID } from '../../../../common/constants'; import { getEndpointListPath } from '../../../management/common/routing'; import { useNavigateToAppEventHandler } from '../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; -import { useManagementFormatUrl } from '../../../management/components/hooks/use_management_format_url'; -import { MANAGEMENT_APP_ID } from '../../../management/common/constants'; export const EndpointNotice = memo<{ onDismiss: () => void }>(({ onDismiss }) => { + const { getUrlForApp } = useKibana().services.application; const endpointsPath = getEndpointListPath({ name: 'endpointList' }); - const endpointsLink = useManagementFormatUrl(endpointsPath); - const handleGetStartedClick = useNavigateToAppEventHandler(MANAGEMENT_APP_ID, { + const endpointsLink = getUrlForApp(APP_ID, { path: endpointsPath }); + const handleGetStartedClick = useNavigateToAppEventHandler(APP_ID, { path: endpointsPath, }); diff --git a/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx b/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx index c93d66f6cbc49..d8ac540d830aa 100644 --- a/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx @@ -94,7 +94,8 @@ const EventsByDatasetComponent: React.FC<Props> = ({ const goToHostEvents = useCallback( (ev) => { ev.preventDefault(); - navigateToApp(`${APP_ID}:${SecurityPageName.hosts}`, { + navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.hosts, path: getTabsOnHostsUrl(HostsTableType.events, urlSearch), }); }, diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx index f11b849f5df6b..0a8e817a3bfc4 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx @@ -16,7 +16,7 @@ import { ESQuery } from '../../../../common/typed_json'; import { ID as OverviewHostQueryId, useHostOverview } from '../../containers/overview_host'; import { HeaderSection } from '../../../common/components/header_section'; import { useUiSetting$, useKibana } from '../../../common/lib/kibana'; -import { getHostsUrl, useFormatUrl } from '../../../common/components/link_to'; +import { getHostDetailsUrl, useFormatUrl } from '../../../common/components/link_to'; import { getOverviewHostStats, OverviewHostStats } from '../overview_host_stats'; import { manageQuery } from '../../../common/components/page/manage_query'; import { InspectButtonContainer } from '../../../common/components/inspect'; @@ -56,8 +56,9 @@ const OverviewHostComponent: React.FC<OverviewHostProps> = ({ const goToHost = useCallback( (ev) => { ev.preventDefault(); - navigateToApp(`${APP_ID}:${SecurityPageName.hosts}`, { - path: getHostsUrl(urlSearch), + navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.hosts, + path: getHostDetailsUrl('allHosts', urlSearch), }); }, [navigateToApp, urlSearch] @@ -75,7 +76,7 @@ const OverviewHostComponent: React.FC<OverviewHostProps> = ({ const hostPageButton = useMemo( () => ( - <LinkButton onClick={goToHost} href={formatUrl(getHostsUrl())}> + <LinkButton onClick={goToHost} href={formatUrl('/allHosts')}> <FormattedMessage id="xpack.securitySolution.overview.hostsAction" defaultMessage="View hosts" diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx index 13a9b529fdf43..08b2392f60488 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx @@ -20,6 +20,7 @@ import { import { OverviewNetwork } from '.'; import { createStore, State } from '../../../common/store'; import { useNetworkOverview } from '../../containers/overview_network'; +import { SecurityPageName } from '../../../app/types'; jest.mock('../../../common/components/link_to'); const mockNavigateToApp = jest.fn(); @@ -137,6 +138,9 @@ describe('OverviewNetwork', () => { preventDefault: jest.fn(), }); - expect(mockNavigateToApp).toBeCalledWith('securitySolution:network', { path: '' }); + expect(mockNavigateToApp).toBeCalledWith('securitySolution', { + path: '', + deepLinkId: SecurityPageName.network, + }); }); }); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx index 39fb6ff08ee53..eb5231d4ce5e0 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx @@ -58,7 +58,8 @@ const OverviewNetworkComponent: React.FC<OverviewNetworkProps> = ({ const goToNetwork = useCallback( (ev) => { ev.preventDefault(); - navigateToApp(`${APP_ID}:${SecurityPageName.network}`, { + navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.network, path: getNetworkUrl(urlSearch), }); }, diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_cases/index.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_cases/index.tsx index cb7733e304985..207c6ef16bd16 100644 --- a/x-pack/plugins/security_solution/public/overview/components/recent_cases/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/recent_cases/index.tsx @@ -14,7 +14,7 @@ import { } from '../../../common/components/link_to/redirect_to_case'; import { useFormatUrl } from '../../../common/components/link_to'; import { useGetUserCasesPermissions, useKibana } from '../../../common/lib/kibana'; -import { APP_ID, CASES_APP_ID } from '../../../../common/constants'; +import { APP_ID } from '../../../../common/constants'; import { SecurityPageName } from '../../../app/types'; import { AllCasesNavProps } from '../../../cases/components/all_cases'; @@ -32,10 +32,8 @@ const RecentCasesComponent = () => { allCasesNavigation: { href: formatUrl(getCaseUrl()), onClick: async (e) => { - if (e) { - e.preventDefault(); - } - return navigateToApp(CASES_APP_ID); + e?.preventDefault(); + return navigateToApp(APP_ID, { deepLinkId: SecurityPageName.case }); }, }, caseDetailsNavigation: { @@ -43,10 +41,9 @@ const RecentCasesComponent = () => { return formatUrl(getCaseDetailsUrl({ id: detailName, subCaseId })); }, onClick: async ({ detailName, subCaseId, search }, e) => { - if (e) { - e.preventDefault(); - } - return navigateToApp(CASES_APP_ID, { + e?.preventDefault(); + return navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.case, path: getCaseDetailsUrl({ id: detailName, search, subCaseId }), }); }, @@ -54,10 +51,9 @@ const RecentCasesComponent = () => { createCaseNavigation: { href: formatUrl(getCreateCaseUrl()), onClick: async (e) => { - if (e) { - e.preventDefault(); - } - return navigateToApp(CASES_APP_ID, { + e?.preventDefault(); + return navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.case, path: getCreateCaseUrl(), }); }, diff --git a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/index.tsx b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/index.tsx index 1d9b039e02258..f76d71600d0e7 100644 --- a/x-pack/plugins/security_solution/public/overview/components/recent_timelines/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/recent_timelines/index.tsx @@ -60,7 +60,9 @@ const StatefulRecentTimelinesComponent: React.FC<Props> = ({ filterBy }) => { const goToTimelines = useCallback( (ev) => { ev.preventDefault(); - navigateToApp(`${APP_ID}:${SecurityPageName.timelines}`); + navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.timelines, + }); }, [navigateToApp] ); diff --git a/x-pack/plugins/security_solution/public/overview/index.ts b/x-pack/plugins/security_solution/public/overview/index.ts index 4ec5b6d7ce48b..3aa6c4185f6da 100644 --- a/x-pack/plugins/security_solution/public/overview/index.ts +++ b/x-pack/plugins/security_solution/public/overview/index.ts @@ -6,14 +6,14 @@ */ import { SecuritySubPlugin } from '../app/types'; -import { OverviewRoutes } from './routes'; +import { routes } from './routes'; export class Overview { public setup() {} public start(): SecuritySubPlugin { return { - SubPluginRoutes: OverviewRoutes, + routes, }; } } diff --git a/x-pack/plugins/security_solution/public/overview/routes.tsx b/x-pack/plugins/security_solution/public/overview/routes.tsx index 7d6fc4858c670..0f83c03f7e3d9 100644 --- a/x-pack/plugins/security_solution/public/overview/routes.tsx +++ b/x-pack/plugins/security_solution/public/overview/routes.tsx @@ -6,14 +6,21 @@ */ import React from 'react'; -import { Route, Switch } from 'react-router-dom'; +import { TrackApplicationView } from '../../../../../src/plugins/usage_collection/public'; +import { OVERVIEW_PATH, SecurityPageName } from '../../common/constants'; +import { SecuritySubPluginRoutes } from '../app/types'; import { Overview } from './pages'; -import { NotFoundPage } from '../app/404'; -export const OverviewRoutes = () => ( - <Switch> - <Route path="/" render={() => <Overview />} /> - <Route render={() => <NotFoundPage />} /> - </Switch> +const OverviewRoutes = () => ( + <TrackApplicationView viewId={SecurityPageName.overview}> + <Overview /> + </TrackApplicationView> ); + +export const routes: SecuritySubPluginRoutes = [ + { + path: OVERVIEW_PATH, + render: OverviewRoutes, + }, +]; diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 32e6748f38141..1bf3edf1605d8 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -18,6 +18,7 @@ import { StartServices, AppObservableLibs, SubPlugins, + StartedSubPlugins, } from './types'; import { AppMountParameters, @@ -35,33 +36,17 @@ import { KibanaServices } from './common/lib/kibana/services'; import { APP_ID, - APP_ICON_SOLUTION, - APP_DETECTIONS_PATH, - APP_HOSTS_PATH, + OVERVIEW_PATH, APP_OVERVIEW_PATH, - APP_NETWORK_PATH, - APP_TIMELINES_PATH, - APP_MANAGEMENT_PATH, - APP_CASES_PATH, APP_PATH, - CASES_APP_ID, DEFAULT_INDEX_KEY, DETECTION_ENGINE_INDEX_URL, DEFAULT_ALERTS_INDEX, + APP_ICON_SOLUTION, } from '../common/constants'; -import { SecurityPageName } from './app/types'; -import { registerDeepLinks, getDeepLinksAndKeywords } from './app/search'; +import { getDeepLinks, updateGlobalNavigation } from './app/deep_links'; import { manageOldSiemRoutes } from './helpers'; -import { - OVERVIEW, - HOSTS, - NETWORK, - TIMELINES, - DETECTION_ENGINE, - CASE, - ADMINISTRATION, -} from './app/translations'; import { IndexFieldsStrategyRequest, IndexFieldsStrategyResponse, @@ -84,10 +69,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S this.config = this.initializerContext.config.get<SecuritySolutionUiConfigType>(); this.kibanaVersion = initializerContext.env.packageInfo.version; } - private detectionsUpdater$ = new Subject<AppUpdater>(); - private hostsUpdater$ = new Subject<AppUpdater>(); - private networkUpdater$ = new Subject<AppUpdater>(); - private caseUpdater$ = new Subject<AppUpdater>(); + private appUpdater$ = new Subject<AppUpdater>(); private storage = new Storage(localStorage); private licensingSubscription: Subscription | null = null; @@ -159,162 +141,26 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S })(); core.application.register({ - exactRoute: true, id: APP_ID, title: APP_NAME, appRoute: APP_PATH, - navLinkStatus: AppNavLinkStatus.hidden, - mount: async () => { - const [{ application }] = await core.getStartServices(); - application.navigateToApp(`${APP_ID}:${SecurityPageName.overview}`, { replace: true }); - return () => true; - }, - }); - - core.application.register({ - id: `${APP_ID}:${SecurityPageName.overview}`, - title: OVERVIEW, - order: 9000, - euiIconType: APP_ICON_SOLUTION, - category: DEFAULT_APP_CATEGORIES.security, - appRoute: APP_OVERVIEW_PATH, - mount: async (params: AppMountParameters) => { - const [coreStart, startPlugins] = await core.getStartServices(); - const { overview: subPlugin } = await this.subPlugins(); - const { renderApp } = await this.lazyApplicationDependencies(); - - return renderApp({ - ...params, - services: await startServices, - store: await this.store(coreStart, startPlugins), - SubPluginRoutes: subPlugin.start().SubPluginRoutes, - }); - }, - }); - - core.application.register({ - id: `${APP_ID}:${SecurityPageName.detections}`, - title: DETECTION_ENGINE, - order: 9001, - euiIconType: APP_ICON_SOLUTION, - category: DEFAULT_APP_CATEGORIES.security, - appRoute: APP_DETECTIONS_PATH, - updater$: this.detectionsUpdater$, - mount: async (params: AppMountParameters) => { - const [coreStart, startPlugins] = await core.getStartServices(); - const { detections: subPlugin } = await this.subPlugins(); - const { renderApp } = await this.lazyApplicationDependencies(); - - return renderApp({ - ...params, - services: await startServices, - store: await this.store(coreStart, startPlugins), - SubPluginRoutes: subPlugin.start(this.storage).SubPluginRoutes, - }); - }, - }); - - core.application.register({ - id: `${APP_ID}:${SecurityPageName.hosts}`, - title: HOSTS, - order: 9002, - euiIconType: APP_ICON_SOLUTION, - category: DEFAULT_APP_CATEGORIES.security, - appRoute: APP_HOSTS_PATH, - updater$: this.hostsUpdater$, - mount: async (params: AppMountParameters) => { - const [coreStart, startPlugins] = await core.getStartServices(); - const { hosts: subPlugin } = await this.subPlugins(); - const { renderApp } = await this.lazyApplicationDependencies(); - return renderApp({ - ...params, - services: await startServices, - store: await this.store(coreStart, startPlugins), - SubPluginRoutes: subPlugin.start(this.storage).SubPluginRoutes, - }); - }, - }); - - core.application.register({ - id: `${APP_ID}:${SecurityPageName.network}`, - title: NETWORK, - order: 9002, - euiIconType: APP_ICON_SOLUTION, - category: DEFAULT_APP_CATEGORIES.security, - appRoute: APP_NETWORK_PATH, - updater$: this.networkUpdater$, - mount: async (params: AppMountParameters) => { - const [coreStart, startPlugins] = await core.getStartServices(); - const { network: subPlugin } = await this.subPlugins(); - const { renderApp } = await this.lazyApplicationDependencies(); - return renderApp({ - ...params, - services: await startServices, - store: await this.store(coreStart, startPlugins), - SubPluginRoutes: subPlugin.start(this.storage).SubPluginRoutes, - }); - }, - }); - - core.application.register({ - id: `${APP_ID}:${SecurityPageName.timelines}`, - title: TIMELINES, - order: 9002, - euiIconType: APP_ICON_SOLUTION, - category: DEFAULT_APP_CATEGORIES.security, - appRoute: APP_TIMELINES_PATH, - ...getDeepLinksAndKeywords(SecurityPageName.timelines), - mount: async (params: AppMountParameters) => { - const [coreStart, startPlugins] = await core.getStartServices(); - const { timelines: subPlugin } = await this.subPlugins(); - const { renderApp } = await this.lazyApplicationDependencies(); - return renderApp({ - ...params, - services: await startServices, - store: await this.store(coreStart, startPlugins), - SubPluginRoutes: subPlugin.start().SubPluginRoutes, - }); - }, - }); - - core.application.register({ - id: CASES_APP_ID, - title: CASE, - order: 9002, - euiIconType: APP_ICON_SOLUTION, category: DEFAULT_APP_CATEGORIES.security, - appRoute: APP_CASES_PATH, - updater$: this.caseUpdater$, - mount: async (params: AppMountParameters) => { - const [coreStart, startPlugins] = await core.getStartServices(); - const { cases: subPlugin } = await this.subPlugins(); - const { renderApp } = await this.lazyApplicationDependencies(); - return renderApp({ - ...params, - services: await startServices, - store: await this.store(coreStart, startPlugins), - SubPluginRoutes: subPlugin.start().SubPluginRoutes, - }); - }, - }); - - core.application.register({ - id: `${APP_ID}:${SecurityPageName.administration}`, - title: ADMINISTRATION, - order: 9002, + navLinkStatus: AppNavLinkStatus.hidden, + searchable: true, + defaultPath: OVERVIEW_PATH, + updater$: this.appUpdater$, euiIconType: APP_ICON_SOLUTION, - category: DEFAULT_APP_CATEGORIES.security, - appRoute: APP_MANAGEMENT_PATH, - ...getDeepLinksAndKeywords(SecurityPageName.administration), + deepLinks: getDeepLinks(), mount: async (params: AppMountParameters) => { const [coreStart, startPlugins] = await core.getStartServices(); - const { management: managementSubPlugin } = await this.subPlugins(); + const subPlugins = await this.startSubPlugins(this.storage, coreStart, startPlugins); const { renderApp } = await this.lazyApplicationDependencies(); return renderApp({ ...params, services: await startServices, - store: await this.store(coreStart, startPlugins), - SubPluginRoutes: managementSubPlugin.start(coreStart, startPlugins).SubPluginRoutes, + store: await this.store(coreStart, startPlugins, subPlugins), + usageCollection: plugins.usageCollection, + subPlugins, }); }, }); @@ -376,17 +222,19 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S if (licensing !== null) { this.licensingSubscription = licensing.subscribe((currentLicense) => { if (currentLicense.type !== undefined) { - registerDeepLinks(SecurityPageName.network, this.networkUpdater$, currentLicense.type); - registerDeepLinks( - SecurityPageName.detections, - this.detectionsUpdater$, - currentLicense.type - ); - registerDeepLinks(SecurityPageName.hosts, this.hostsUpdater$, currentLicense.type); - registerDeepLinks(SecurityPageName.case, this.caseUpdater$, currentLicense.type); + this.appUpdater$.next(() => ({ + navLinkStatus: AppNavLinkStatus.hidden, // workaround to prevent main navLink to switch to visible after update. should not be needed + deepLinks: getDeepLinks(currentLicense.type, core.application.capabilities), + })); } }); + } else { + updateGlobalNavigation({ + capabilities: core.application.capabilities, + updater$: this.appUpdater$, + }); } + return {}; } @@ -434,7 +282,9 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S if (!this._subPlugins) { const { subPluginClasses } = await this.lazySubPlugins(); this._subPlugins = { - detections: new subPluginClasses.Detections(), + alerts: new subPluginClasses.Detections(), + rules: new subPluginClasses.Rules(), + exceptions: new subPluginClasses.Exceptions(), cases: new subPluginClasses.Cases(), hosts: new subPluginClasses.Hosts(), network: new subPluginClasses.Network(), @@ -446,10 +296,36 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S return this._subPlugins; } + /** + * All started subPlugins. + */ + private async startSubPlugins( + storage: Storage, + core: CoreStart, + plugins: StartPlugins + ): Promise<StartedSubPlugins> { + const subPlugins = await this.subPlugins(); + return { + overview: subPlugins.overview.start(), + alerts: subPlugins.alerts.start(storage), + rules: subPlugins.rules.start(storage), + exceptions: subPlugins.exceptions.start(storage), + cases: subPlugins.cases.start(), + hosts: subPlugins.hosts.start(storage), + network: subPlugins.network.start(storage), + timelines: subPlugins.timelines.start(), + management: subPlugins.management.start(core, plugins), + }; + } + /** * Lazily instantiate a `SecurityAppStore`. We lazily instantiate this because it requests large dynamic imports. We instantiate it once because each subPlugin needs to share the same reference. */ - private async store(coreStart: CoreStart, startPlugins: StartPlugins): Promise<SecurityAppStore> { + private async store( + coreStart: CoreStart, + startPlugins: StartPlugins, + subPlugins: StartedSubPlugins + ): Promise<SecurityAppStore> { if (!this._store) { const experimentalFeatures = parseExperimentalConfigValue( this.config.enableExperimental || [] @@ -458,18 +334,10 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S const [ { createStore, createInitialState }, kibanaIndexPatterns, - { - detections: detectionsSubPlugin, - hosts: hostsSubPlugin, - network: networkSubPlugin, - timelines: timelinesSubPlugin, - management: managementSubPlugin, - }, configIndexPatterns, ] = await Promise.all([ this.lazyApplicationDependencies(), startPlugins.data.indexPatterns.getIdsWithTitle(), - this.subPlugins(), startPlugins.data.search .search<IndexFieldsStrategyRequest, IndexFieldsStrategyResponse>( { indices: defaultIndicesName, onlyCheckIfIndicesExist: true }, @@ -498,19 +366,16 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S const appLibs: AppObservableLibs = { kibana: coreStart }; const libs$ = new BehaviorSubject(appLibs); - const detectionsStart = detectionsSubPlugin.start(this.storage); - const hostsStart = hostsSubPlugin.start(this.storage); - const networkStart = networkSubPlugin.start(this.storage); - const timelinesStart = timelinesSubPlugin.start(); - const managementSubPluginStart = managementSubPlugin.start(coreStart, startPlugins); const timelineInitialState = { timeline: { - ...timelinesStart.store.initialState.timeline!, + ...subPlugins.timelines.store.initialState.timeline!, timelineById: { - ...timelinesStart.store.initialState.timeline!.timelineById, - ...detectionsStart.storageTimelines!.timelineById, - ...hostsStart.storageTimelines!.timelineById, - ...networkStart.storageTimelines!.timelineById, + ...subPlugins.timelines.store.initialState.timeline!.timelineById, + ...subPlugins.alerts.storageTimelines!.timelineById, + ...subPlugins.rules.storageTimelines!.timelineById, + ...subPlugins.exceptions.storageTimelines!.timelineById, + ...subPlugins.hosts.storageTimelines!.timelineById, + ...subPlugins.network.storageTimelines!.timelineById, }, }, }; @@ -519,16 +384,16 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S const timelineReducer = (reduceReducers( timelineInitialState.timeline, tGridReducer, - timelinesStart.store.reducer.timeline + subPlugins.timelines.store.reducer.timeline ) as unknown) as Reducer<TimelineState, AnyAction>; this._store = createStore( createInitialState( { - ...hostsStart.store.initialState, - ...networkStart.store.initialState, + ...subPlugins.hosts.store.initialState, + ...subPlugins.network.store.initialState, ...timelineInitialState, - ...managementSubPluginStart.store.initialState, + ...subPlugins.management.store.initialState, }, { kibanaIndexPatterns, @@ -538,15 +403,15 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S } ), { - ...hostsStart.store.reducer, - ...networkStart.store.reducer, + ...subPlugins.hosts.store.reducer, + ...subPlugins.network.store.reducer, timeline: timelineReducer, - ...managementSubPluginStart.store.reducer, + ...subPlugins.management.store.reducer, ...tGridReducer, }, libs$.pipe(pluck('kibana')), this.storage, - [...(managementSubPluginStart.store.middleware ?? [])] + [...(subPlugins.management.store.middleware ?? [])] ); if (startPlugins.timelines) { startPlugins.timelines.setTGridEmbeddedStore(this._store); diff --git a/x-pack/plugins/security_solution/public/rules/index.ts b/x-pack/plugins/security_solution/public/rules/index.ts new file mode 100644 index 0000000000000..e74efabad4932 --- /dev/null +++ b/x-pack/plugins/security_solution/public/rules/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; + +import { SecuritySubPlugin } from '../app/types'; +import { DETECTIONS_TIMELINE_IDS } from '../detections'; +import { getTimelinesInStorageByIds } from '../timelines/containers/local_storage'; +import { routes } from './routes'; + +export class Rules { + public setup() {} + + public start(storage: Storage): SecuritySubPlugin { + return { + storageTimelines: { + timelineById: getTimelinesInStorageByIds(storage, DETECTIONS_TIMELINE_IDS), + }, + routes, + }; + } +} diff --git a/x-pack/plugins/security_solution/public/rules/routes.tsx b/x-pack/plugins/security_solution/public/rules/routes.tsx new file mode 100644 index 0000000000000..39b882ad76f8c --- /dev/null +++ b/x-pack/plugins/security_solution/public/rules/routes.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { Route, Switch } from 'react-router-dom'; + +import { TrackApplicationView } from '../../../../../src/plugins/usage_collection/public'; +import { RULES_PATH, SecurityPageName } from '../../common/constants'; +import { RulesPage } from '../detections/pages/detection_engine/rules'; +import { CreateRulePage } from '../detections/pages/detection_engine/rules/create'; +import { RuleDetailsPage } from '../detections/pages/detection_engine/rules/details'; +import { EditRulePage } from '../detections/pages/detection_engine/rules/edit'; + +const RulesSubRoutes = [ + { + path: '/rules/id/:detailName/edit', + main: EditRulePage, + }, + { + path: '/rules/id/:detailName', + main: RuleDetailsPage, + }, + { + path: '/rules/create', + main: CreateRulePage, + }, + { + path: '/rules', + exact: true, + main: RulesPage, + }, +]; + +export const RulesRoutes = () => { + return ( + <TrackApplicationView viewId={SecurityPageName.rules}> + <Switch> + {RulesSubRoutes.map((route, index) => ( + <Route key={`rules-route-${route.path}`} path={route.path} exact={route?.exact ?? false}> + <route.main /> + </Route> + ))} + </Switch> + </TrackApplicationView> + ); +}; + +export const routes = [ + { + path: RULES_PATH, + render: RulesRoutes, + }, +]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.test.tsx index bc9876b207284..717b338aa2535 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.test.tsx @@ -12,6 +12,7 @@ import { useKibana } from '../../../../common/lib/kibana'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { mockTimelineModel, TestProviders } from '../../../../common/mock'; import { AddToCaseButton } from '.'; +import { SecurityPageName } from '../../../../../common/constants'; jest.mock('../../../../common/components/link_to', () => { const original = jest.requireActual('../../../../common/components/link_to'); @@ -61,7 +62,10 @@ describe('AddToCaseButton', () => { wrapper.find(`[data-test-subj="attach-timeline-case-button"]`).first().simulate('click'); wrapper.find(`[data-test-subj="attach-timeline-existing-case"]`).first().simulate('click'); - expect(navigateToApp).toHaveBeenCalledWith('securitySolution:case', { path: '/create' }); + expect(navigateToApp).toHaveBeenCalledWith('securitySolution', { + path: '/create', + deepLinkId: SecurityPageName.case, + }); }); it('navigates to the correct path with id', async () => { @@ -80,6 +84,9 @@ describe('AddToCaseButton', () => { wrapper.find(`[data-test-subj="attach-timeline-case-button"]`).first().simulate('click'); wrapper.find(`[data-test-subj="attach-timeline-existing-case"]`).first().simulate('click'); - expect(navigateToApp).toHaveBeenCalledWith('securitySolution:case', { path: '/case-id' }); + expect(navigateToApp).toHaveBeenCalledWith('securitySolution', { + path: '/case-id', + deepLinkId: SecurityPageName.case, + }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx index 47935347b96ac..553b827f2a64c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/add_to_case_button/index.tsx @@ -11,7 +11,7 @@ import React, { useCallback, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; import { Case, SubCase } from '../../../../../../cases/common'; -import { APP_ID, CASES_APP_ID } from '../../../../../common/constants'; +import { APP_ID } from '../../../../../common/constants'; import { timelineSelectors } from '../../../../timelines/store/timeline'; import { setInsertTimeline, showTimeline } from '../../../store/timeline/actions'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; @@ -55,7 +55,8 @@ const AddToCaseButtonComponent: React.FC<Props> = ({ timelineId }) => { const onRowClick = useCallback( async (theCase?: Case | SubCase) => { openCaseModal(false); - await navigateToApp(CASES_APP_ID, { + await navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.case, path: theCase != null ? getCaseDetailsUrl({ id: theCase.id }) : getCreateCaseUrl(), }); dispatch( @@ -88,7 +89,9 @@ const AddToCaseButtonComponent: React.FC<Props> = ({ timelineId }) => { const handleNewCaseClick = useCallback(() => { handlePopoverClose(); - navigateToApp(CASES_APP_ID, { + + navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.case, path: getCreateCaseUrl(), }).then(() => { dispatch( diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx index 35a13aba471fd..14ebfbc20d9c9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiFlyout } from '@elastic/eui'; +import { EuiFlyout, EuiFlyoutProps } from '@elastic/eui'; import React, { useCallback } from 'react'; import styled from 'styled-components'; import { useDispatch } from 'react-redux'; @@ -23,13 +23,8 @@ interface FlyoutPaneComponentProps { visible?: boolean; } -const EuiFlyoutContainer = styled.div` - .timeline-flyout { - z-index: ${({ theme }) => theme.eui.euiZLevel8}; - min-width: 150px; - width: 100%; - animation: none; - } +const StyledEuiFlyout = styled(EuiFlyout)<EuiFlyoutProps>` + animation: none; `; const FlyoutPaneComponent: React.FC<FlyoutPaneComponentProps> = ({ @@ -43,17 +38,14 @@ const FlyoutPaneComponent: React.FC<FlyoutPaneComponentProps> = ({ }, [dispatch, timelineId]); return ( - <EuiFlyoutContainer - data-test-subj="flyout-pane" - style={{ visibility: visible ? 'visible' : 'hidden' }} - > - <EuiFlyout + <div data-test-subj="flyout-pane" style={{ visibility: visible ? 'visible' : 'hidden' }}> + <StyledEuiFlyout aria-label={i18n.TIMELINE_DESCRIPTION} className="timeline-flyout" data-test-subj="eui-flyout" hideCloseButton={true} onClose={handleClose} - size="l" + size="100%" ownFocus={false} style={{ visibility: visible ? 'visible' : 'hidden' }} > @@ -62,8 +54,8 @@ const FlyoutPaneComponent: React.FC<FlyoutPaneComponentProps> = ({ rowRenderers={defaultRowRenderers} timelineId={timelineId} /> - </EuiFlyout> - </EuiFlyoutContainer> + </StyledEuiFlyout> + </div> ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.test.tsx index 1d39dd169ffaa..5402210d22cce 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.test.tsx @@ -31,6 +31,22 @@ jest.mock('../../../common/components/link_to', () => { }; }); +jest.mock('../../../../../../../src/plugins/kibana_react/public', () => { + const originalModule = jest.requireActual('../../../../../../../src/plugins/kibana_react/public'); + const useKibana = jest.fn().mockImplementation(() => ({ + services: { + application: { + navigateToUrl: jest.fn(), + }, + }, + })); + + return { + ...originalModule, + useKibana, + }; +}); + describe('useTimelineTypes', () => { it('init', async () => { await act(async () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx index a66fe43d305f1..ca8b443309e12 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx @@ -6,7 +6,7 @@ */ import React, { useState, useCallback, useMemo } from 'react'; -import { useParams, useHistory } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; import { EuiTabs, EuiTab, EuiSpacer } from '@elastic/eui'; import { noop } from 'lodash/fp'; @@ -15,7 +15,7 @@ import { SecurityPageName } from '../../../app/types'; import { getTimelineTabsUrl, useFormatUrl } from '../../../common/components/link_to'; import * as i18n from './translations'; import { TimelineTabsStyle, TimelineTab } from './types'; - +import { useKibana } from '../../../common/lib/kibana'; export interface UseTimelineTypesArgs { defaultTimelineCount?: number | null; templateTimelineCount?: number | null; @@ -31,8 +31,8 @@ export const useTimelineTypes = ({ defaultTimelineCount, templateTimelineCount, }: UseTimelineTypesArgs): UseTimelineTypesResult => { - const history = useHistory(); const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.timelines); + const { navigateToUrl } = useKibana().services.application; const { tabName } = useParams<{ pageName: SecurityPageName; tabName: string }>(); const [timelineType, setTimelineTypes] = useState<TimelineTypeLiteralWithNull>( tabName === TimelineType.default || tabName === TimelineType.template @@ -40,27 +40,30 @@ export const useTimelineTypes = ({ : TimelineType.default ); + const timelineUrl = formatUrl(getTimelineTabsUrl(TimelineType.default, urlSearch)); + const templateUrl = formatUrl(getTimelineTabsUrl(TimelineType.template, urlSearch)); + const goToTimeline = useCallback( (ev) => { ev.preventDefault(); - history.push(getTimelineTabsUrl(TimelineType.default, urlSearch)); + navigateToUrl(timelineUrl); }, - [history, urlSearch] + [navigateToUrl, timelineUrl] ); const goToTemplateTimeline = useCallback( (ev) => { ev.preventDefault(); - history.push(getTimelineTabsUrl(TimelineType.template, urlSearch)); + navigateToUrl(templateUrl); }, - [history, urlSearch] + [navigateToUrl, templateUrl] ); const getFilterOrTabs: (timelineTabsStyle: TimelineTabsStyle) => TimelineTab[] = useCallback( (timelineTabsStyle: TimelineTabsStyle) => [ { id: TimelineType.default, name: i18n.TAB_TIMELINES, - href: formatUrl(getTimelineTabsUrl(TimelineType.default, urlSearch)), + href: timelineUrl, disabled: false, onClick: timelineTabsStyle === TimelineTabsStyle.tab ? goToTimeline : noop, @@ -68,13 +71,13 @@ export const useTimelineTypes = ({ { id: TimelineType.template, name: i18n.TAB_TEMPLATES, - href: formatUrl(getTimelineTabsUrl(TimelineType.template, urlSearch)), + href: templateUrl, disabled: false, onClick: timelineTabsStyle === TimelineTabsStyle.tab ? goToTemplateTimeline : noop, }, ], - [urlSearch, formatUrl, goToTimeline, goToTemplateTimeline] + [timelineUrl, templateUrl, goToTimeline, goToTemplateTimeline] ); const onFilterClicked = useCallback( diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap index 4c8139a78b012..06db698a91a6d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap @@ -274,12 +274,14 @@ Array [ <Styled(EuiFlyout) data-test-subj="timeline:details-panel:flyout" onClose={[Function]} + ownFocus={false} size="m" > <EuiFlyout className="c0" data-test-subj="timeline:details-panel:flyout" onClose={[Function]} + ownFocus={false} size="m" > <div @@ -507,6 +509,7 @@ Array [ className="c0" data-test-subj="timeline:details-panel:flyout" onClose={[Function]} + ownFocus={false} size="m" > <div diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.tsx index 629bdcca98640..ea408be7c8e9a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/index.tsx @@ -117,6 +117,7 @@ export const DetailsPanel = React.memo( data-test-subj="timeline:details-panel:flyout" size={panelSize} onClose={closePanel} + ownFocus={false} > {visiblePanel} </StyledEuiFlyout> diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx index d33192528a090..ef509cdfbda17 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx @@ -48,14 +48,15 @@ export const RenderRuleName: React.FC<RenderRuleNameProps> = ({ }) => { const ruleName = `${value}`; const ruleId = linkValue; - const { search } = useFormatUrl(SecurityPageName.detections); + const { search } = useFormatUrl(SecurityPageName.rules); const { navigateToApp, getUrlForApp } = useKibana().services.application; const content = truncate ? <TruncatableText>{value}</TruncatableText> : value; const goToRuleDetails = useCallback( (ev) => { ev.preventDefault(); - navigateToApp(`${APP_ID}:${SecurityPageName.detections}`, { + navigateToApp(APP_ID, { + deepLinkId: SecurityPageName.rules, path: getRuleDetailsUrl(ruleId ?? '', search), }); }, @@ -71,7 +72,8 @@ export const RenderRuleName: React.FC<RenderRuleNameProps> = ({ > <LinkAnchor onClick={goToRuleDetails} - href={getUrlForApp(`${APP_ID}:${SecurityPageName.detections}`, { + href={getUrlForApp(APP_ID, { + deepLinkId: SecurityPageName.rules, path: getRuleDetailsUrl(ruleId, search), })} > diff --git a/x-pack/plugins/security_solution/public/timelines/index.ts b/x-pack/plugins/security_solution/public/timelines/index.ts index 224eec7568c6b..8725072e61849 100644 --- a/x-pack/plugins/security_solution/public/timelines/index.ts +++ b/x-pack/plugins/security_solution/public/timelines/index.ts @@ -6,7 +6,7 @@ */ import { SecuritySubPluginWithStore } from '../app/types'; -import { TimelinesRoutes } from './routes'; +import { routes } from './routes'; import { initialTimelineState, timelineReducer } from './store/timeline/reducer'; import { TimelineState } from './store/timeline/types'; @@ -15,7 +15,7 @@ export class Timelines { public start(): SecuritySubPluginWithStore<'timeline', TimelineState> { return { - SubPluginRoutes: TimelinesRoutes, + routes, store: { initialState: { timeline: initialTimelineState }, reducer: { timeline: timelineReducer }, diff --git a/x-pack/plugins/security_solution/public/timelines/pages/index.tsx b/x-pack/plugins/security_solution/public/timelines/pages/index.tsx index 806ac57df1f65..2bf6e1259ff75 100644 --- a/x-pack/plugins/security_solution/public/timelines/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/pages/index.tsx @@ -7,7 +7,7 @@ import { isEmpty } from 'lodash/fp'; import React from 'react'; -import { Switch, Route, useHistory } from 'react-router-dom'; +import { Switch, Route, Redirect } from 'react-router-dom'; import { ChromeBreadcrumb } from '../../../../../../src/core/public'; @@ -18,11 +18,11 @@ import { TimelinesPage } from './timelines_page'; import { PAGE_TITLE } from './translations'; import { appendSearch } from '../../common/components/link_to/helpers'; import { GetUrlForApp } from '../../common/components/navigation/types'; -import { APP_ID } from '../../../common/constants'; +import { APP_ID, TIMELINES_PATH } from '../../../common/constants'; import { SecurityPageName } from '../../app/types'; -const timelinesPagePath = `/:tabName(${TimelineType.default}|${TimelineType.template})`; -const timelinesDefaultPath = `/${TimelineType.default}`; +const timelinesPagePath = `${TIMELINES_PATH}/:tabName(${TimelineType.default}|${TimelineType.template})`; +const timelinesDefaultPath = `${TIMELINES_PATH}/${TimelineType.default}`; export const getBreadcrumbs = ( params: TimelineRouteSpyState, @@ -31,28 +31,25 @@ export const getBreadcrumbs = ( ): ChromeBreadcrumb[] => [ { text: PAGE_TITLE, - href: getUrlForApp(`${APP_ID}:${SecurityPageName.timelines}`, { + href: getUrlForApp(APP_ID, { + deepLinkId: SecurityPageName.timelines, path: !isEmpty(search[0]) ? search[0] : '', }), }, ]; -export const Timelines = React.memo(() => { - const history = useHistory(); - return ( - <Switch> - <Route exact path={timelinesPagePath}> - <TimelinesPage /> - </Route> - <Route - path="/" - render={({ location: { search = '' } }) => { - history.replace(`${timelinesDefaultPath}${appendSearch(search)}`); - return null; - }} - /> - </Switch> - ); -}); +export const Timelines = React.memo(() => ( + <Switch> + <Route exact path={timelinesPagePath}> + <TimelinesPage /> + </Route> + <Route + path={TIMELINES_PATH} + render={({ location: { search = '' } }) => ( + <Redirect to={`${timelinesDefaultPath}${appendSearch(search)}`} /> + )} + /> + </Switch> +)); Timelines.displayName = 'Timelines'; diff --git a/x-pack/plugins/security_solution/public/timelines/routes.tsx b/x-pack/plugins/security_solution/public/timelines/routes.tsx index 010a022bfefb7..0bc95b3b4959f 100644 --- a/x-pack/plugins/security_solution/public/timelines/routes.tsx +++ b/x-pack/plugins/security_solution/public/timelines/routes.tsx @@ -6,20 +6,21 @@ */ import React from 'react'; -import { Route, Switch } from 'react-router-dom'; - +import { TrackApplicationView } from '../../../../../src/plugins/usage_collection/public'; import { Timelines } from './pages'; -import { NotFoundPage } from '../app/404'; +import { TIMELINES_PATH } from '../../common/constants'; + +import { SecurityPageName, SecuritySubPluginRoutes } from '../app/types'; -const TimelinesRoutesComponent = () => ( - <Switch> - <Route path="/"> - <Timelines /> - </Route> - <Route> - <NotFoundPage /> - </Route> - </Switch> +const TimelinesRoutes = () => ( + <TrackApplicationView viewId={SecurityPageName.timelines}> + <Timelines /> + </TrackApplicationView> ); -export const TimelinesRoutes = React.memo(TimelinesRoutesComponent); +export const routes: SecuritySubPluginRoutes = [ + { + path: TIMELINES_PATH, + render: TimelinesRoutes, + }, +]; diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index aad685f9fb103..98187b7d25f5c 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -30,9 +30,11 @@ import { MlPluginSetup, MlPluginStart } from '../../ml/public'; import { Detections } from './detections'; import { Cases } from './cases'; +import { Exceptions } from './exceptions'; import { Hosts } from './hosts'; import { Network } from './network'; import { Overview } from './overview'; +import { Rules } from './rules'; import { Timelines } from './timelines'; import { Management } from './management'; import { LicensingPluginStart, LicensingPluginSetup } from '../../licensing/public'; @@ -81,7 +83,9 @@ export interface AppObservableLibs { export type InspectResponse = Inspect & { response: string[] }; export interface SubPlugins { - detections: Detections; + alerts: Detections; + rules: Rules; + exceptions: Exceptions; cases: Cases; hosts: Hosts; network: Network; @@ -89,3 +93,16 @@ export interface SubPlugins { timelines: Timelines; management: Management; } + +// TODO: find a better way to defined these types +export interface StartedSubPlugins { + alerts: ReturnType<Detections['start']>; + rules: ReturnType<Rules['start']>; + exceptions: ReturnType<Exceptions['start']>; + cases: ReturnType<Cases['start']>; + hosts: ReturnType<Hosts['start']>; + network: ReturnType<Network['start']>; + overview: ReturnType<Overview['start']>; + timelines: ReturnType<Timelines['start']>; + management: ReturnType<Management['start']>; +} diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 4bcbcb71d048c..9f8b1923ff5b8 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -65,7 +65,6 @@ import { initUiSettings } from './ui_settings'; import { APP_ID, SERVER_APP_ID, - SecurityPageName, SIGNALS_ID, NOTIFICATIONS_ID, REFERENCE_RULE_ALERT_TYPE_ID, @@ -125,24 +124,6 @@ export interface PluginSetup {} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface PluginStart {} - -const casesSubPlugin = `${APP_ID}:${SecurityPageName.case}`; - -/** - * Don't include cases here so that the sub feature can govern whether Cases is enabled in the navigation - */ -const securitySubPluginsNoCases = [ - APP_ID, - `${APP_ID}:${SecurityPageName.overview}`, - `${APP_ID}:${SecurityPageName.detections}`, - `${APP_ID}:${SecurityPageName.hosts}`, - `${APP_ID}:${SecurityPageName.network}`, - `${APP_ID}:${SecurityPageName.timelines}`, - `${APP_ID}:${SecurityPageName.administration}`, -]; - -const allSecuritySubPlugins = [...securitySubPluginsNoCases, casesSubPlugin]; - export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, StartPlugins> { private readonly logger: Logger; private readonly config: ConfigType; @@ -308,7 +289,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S }), order: 1100, category: DEFAULT_APP_CATEGORIES.security, - app: [...allSecuritySubPlugins, 'kibana'], + app: [APP_ID, 'kibana'], catalogue: ['securitySolution'], management: { insightsAndAlerting: ['triggersActions'], @@ -323,9 +304,6 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S groupType: 'mutually_exclusive', privileges: [ { - // if the user is granted access to the cases feature than the global nav will show the cases - // sub plugin within the security solution navigation - app: [casesSubPlugin], id: 'cases_all', includeIn: 'all', name: 'All', @@ -341,7 +319,6 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S }, }, { - app: [casesSubPlugin], id: 'cases_read', includeIn: 'read', name: 'Read', @@ -363,7 +340,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S ], privileges: { all: { - app: [...securitySubPluginsNoCases, 'kibana'], + app: [APP_ID, 'kibana'], catalogue: ['securitySolution'], api: ['securitySolution', 'lists-all', 'lists-read'], savedObject: { @@ -384,7 +361,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S ui: ['show', 'crud'], }, read: { - app: [...securitySubPluginsNoCases, 'kibana'], + app: [APP_ID, 'kibana'], catalogue: ['securitySolution'], api: ['securitySolution', 'lists-read'], savedObject: { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 27f5abc6aae81..05377322e5bce 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -20584,7 +20584,6 @@ "xpack.securitySolution.navigation.administration": "管理", "xpack.securitySolution.navigation.alerts": "アラート", "xpack.securitySolution.navigation.case": "ケース", - "xpack.securitySolution.navigation.detectionEngine": "検出", "xpack.securitySolution.navigation.hosts": "ホスト", "xpack.securitySolution.navigation.network": "ネットワーク", "xpack.securitySolution.navigation.overview": "概要", @@ -20904,8 +20903,6 @@ "xpack.securitySolution.search.cases": "ケース", "xpack.securitySolution.search.cases.configure": "ケースを構成", "xpack.securitySolution.search.cases.create": "新規ケースを作成", - "xpack.securitySolution.search.detections": "検出", - "xpack.securitySolution.search.detections.manage": "ルールの管理", "xpack.securitySolution.search.hosts": "ホスト", "xpack.securitySolution.search.hosts.anomalies": "異常", "xpack.securitySolution.search.hosts.authentications": "認証", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 9883debe8ebd5..2e48e5e387c38 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -20896,7 +20896,6 @@ "xpack.securitySolution.navigation.administration": "管理", "xpack.securitySolution.navigation.alerts": "告警", "xpack.securitySolution.navigation.case": "案例", - "xpack.securitySolution.navigation.detectionEngine": "检测", "xpack.securitySolution.navigation.hosts": "主机", "xpack.securitySolution.navigation.network": "网络", "xpack.securitySolution.navigation.overview": "概览", @@ -21236,8 +21235,6 @@ "xpack.securitySolution.search.cases": "案例", "xpack.securitySolution.search.cases.configure": "配置案例", "xpack.securitySolution.search.cases.create": "创建新案例", - "xpack.securitySolution.search.detections": "检测", - "xpack.securitySolution.search.detections.manage": "管理规则", "xpack.securitySolution.search.hosts": "主机", "xpack.securitySolution.search.hosts.anomalies": "异常", "xpack.securitySolution.search.hosts.authentications": "身份验证",