From 44c70915076e4bc74817fcb8830fac9aa5b34c7e Mon Sep 17 00:00:00 2001 From: Sergi Massaneda Date: Mon, 3 Jul 2023 18:13:35 +0200 Subject: [PATCH 1/3] [SecuritySolution] Project breadcrumbs (#160784) ## Summary - Integrate new chrome project breadcrumbs API and "navigation tree" API on `serverless_plugin`. - test with: `yarn serverless-security` - Ess implementation migrated to `ess_security` plugin, everything should work the same way. Project breadcrumbs implementation https://github.com/elastic/kibana/pull/160252 ![screenshot](https://github.com/elastic/kibana/assets/17747913/d6533184-1bc8-4596-a3e1-4d815984f654) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/cases/kibana.jsonc | 3 +- .../common/lib/kibana/__mocks__/index.ts | 2 +- .../public/common/lib/kibana/services.ts | 7 +- .../components/all_cases/index.test.tsx | 1 + .../public/components/create/index.test.tsx | 1 + .../components/use_breadcrumbs/index.test.tsx | 29 + .../components/use_breadcrumbs/index.ts | 11 +- x-pack/plugins/cases/public/types.ts | 3 + x-pack/plugins/cases/tsconfig.json | 1 + .../public/breadcrumbs/breadcrumbs.test.ts | 33 ++ .../public/breadcrumbs/breadcrumbs.ts | 29 + .../ess_security/public/breadcrumbs/index.ts | 7 + .../ess_security/public/common/jest.config.js | 26 - .../public/get_started/jest.config.js | 16 - .../ess_security/public/jest.config.js | 2 +- x-pack/plugins/ess_security/public/plugin.ts | 7 +- .../cloud_security_posture/breadcrumbs.ts | 14 +- .../public/common/breadcrumbs/breadcrumbs.ts | 22 + .../public/common/breadcrumbs/index.ts | 9 + .../public/common/breadcrumbs/types.ts | 13 + .../breadcrumbs/get_breadcrumbs_for_page.ts | 33 -- .../navigation/breadcrumbs/index.test.ts | 549 ------------------ .../navigation/breadcrumbs/index.ts | 134 +---- .../breadcrumbs/trailing_breadcrumbs.ts | 48 ++ .../navigation/breadcrumbs/types.ts} | 15 +- .../breadcrumbs/use_breadcrumbs_nav.test.ts | 150 +++++ .../breadcrumbs/use_breadcrumbs_nav.ts | 87 +++ .../use_security_solution_navigation.test.tsx | 3 +- .../use_security_solution_navigation.tsx | 6 +- .../public/dashboards/pages/breadcrumbs.ts | 22 + .../pages/alert_details/utils/breadcrumbs.ts | 15 +- .../detection_engine/rules/breadcrumbs.ts | 92 +++ .../pages/detection_engine/rules/utils.ts | 85 +-- .../utils/{pages.utils.ts => breadcrumbs.ts} | 13 +- .../details/{utils.ts => breadcrumbs.ts} | 18 +- .../hosts/pages/details/details_tabs.test.tsx | 5 +- .../hosts/pages/details/details_tabs.tsx | 5 +- .../explore/hosts/pages/details/index.tsx | 4 +- .../details/{utils.ts => breadcrumbs.ts} | 18 +- .../explore/network/pages/details/index.tsx | 2 - .../details/{utils.ts => breadcrumbs.ts} | 18 +- .../explore/users/pages/details/index.tsx | 3 +- .../plugins/security_solution/public/index.ts | 2 + .../kubernetes/pages/utils/breadcrumbs.ts | 13 +- .../plugins/security_solution/public/mocks.ts | 5 + .../security_solution/public/plugin.tsx | 45 +- .../public/plugin_contract.ts | 67 +++ .../plugins/security_solution/public/types.ts | 16 +- .../plugins/security_solution/tsconfig.json | 1 - .../public/common/jest.config.js | 28 + .../public/common/navigation/breadcrumbs.ts | 15 + .../public/common/navigation/links/index.ts | 8 + .../common/navigation/links/nav_links.ts | 18 + .../public/common/navigation/links/types.ts | 16 + .../common/navigation/navigation_tree.test.ts | 286 +++++++++ .../common/navigation/navigation_tree.ts | 86 +++ .../public/{ => common}/services.mock.tsx | 8 +- .../public/{ => common}/services.tsx | 23 +- .../public/components/get_started/index.tsx | 10 +- .../get_started/toggle_panel.test.tsx | 8 - .../components/side_navigation/index.tsx | 18 +- .../side_navigation/side_navigation.test.tsx | 2 +- .../public/hooks/use_link_props.test.tsx | 6 +- .../public/hooks/use_link_props.ts | 2 +- .../public/hooks/use_nav_links.ts | 9 +- .../public/hooks/use_side_nav_items.test.tsx | 21 +- .../public/hooks/use_side_nav_items.ts | 7 +- .../serverless_security/public/plugin.ts | 16 +- 68 files changed, 1266 insertions(+), 1031 deletions(-) create mode 100644 x-pack/plugins/ess_security/public/breadcrumbs/breadcrumbs.test.ts create mode 100644 x-pack/plugins/ess_security/public/breadcrumbs/breadcrumbs.ts create mode 100644 x-pack/plugins/ess_security/public/breadcrumbs/index.ts delete mode 100644 x-pack/plugins/ess_security/public/common/jest.config.js delete mode 100644 x-pack/plugins/ess_security/public/get_started/jest.config.js create mode 100644 x-pack/plugins/security_solution/public/common/breadcrumbs/breadcrumbs.ts create mode 100644 x-pack/plugins/security_solution/public/common/breadcrumbs/index.ts create mode 100644 x-pack/plugins/security_solution/public/common/breadcrumbs/types.ts delete mode 100644 x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/get_breadcrumbs_for_page.ts delete mode 100644 x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/trailing_breadcrumbs.ts rename x-pack/plugins/security_solution/public/{dashboards/pages/utils.ts => common/components/navigation/breadcrumbs/types.ts} (51%) create mode 100644 x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/use_breadcrumbs_nav.test.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/use_breadcrumbs_nav.ts create mode 100644 x-pack/plugins/security_solution/public/dashboards/pages/breadcrumbs.ts create mode 100644 x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/breadcrumbs.ts rename x-pack/plugins/security_solution/public/exceptions/utils/{pages.utils.ts => breadcrumbs.ts} (61%) rename x-pack/plugins/security_solution/public/explore/hosts/pages/details/{utils.ts => breadcrumbs.ts} (78%) rename x-pack/plugins/security_solution/public/explore/network/pages/details/{utils.ts => breadcrumbs.ts} (79%) rename x-pack/plugins/security_solution/public/explore/users/pages/details/{utils.ts => breadcrumbs.ts} (77%) create mode 100644 x-pack/plugins/security_solution/public/plugin_contract.ts create mode 100644 x-pack/plugins/serverless_security/public/common/jest.config.js create mode 100644 x-pack/plugins/serverless_security/public/common/navigation/breadcrumbs.ts create mode 100644 x-pack/plugins/serverless_security/public/common/navigation/links/index.ts create mode 100644 x-pack/plugins/serverless_security/public/common/navigation/links/nav_links.ts create mode 100644 x-pack/plugins/serverless_security/public/common/navigation/links/types.ts create mode 100644 x-pack/plugins/serverless_security/public/common/navigation/navigation_tree.test.ts create mode 100644 x-pack/plugins/serverless_security/public/common/navigation/navigation_tree.ts rename x-pack/plugins/serverless_security/public/{ => common}/services.mock.tsx (74%) rename x-pack/plugins/serverless_security/public/{ => common}/services.tsx (56%) diff --git a/x-pack/plugins/cases/kibana.jsonc b/x-pack/plugins/cases/kibana.jsonc index 6604dc63402ef..b0a03ad753e97 100644 --- a/x-pack/plugins/cases/kibana.jsonc +++ b/x-pack/plugins/cases/kibana.jsonc @@ -35,7 +35,8 @@ "home", "taskManager", "usageCollection", - "spaces" + "spaces", + "serverless", ], "requiredBundles": [], "extraPublicDirs": [ diff --git a/x-pack/plugins/cases/public/common/lib/kibana/__mocks__/index.ts b/x-pack/plugins/cases/public/common/lib/kibana/__mocks__/index.ts index 5062d73dd9b2f..3aa4c02457ef7 100644 --- a/x-pack/plugins/cases/public/common/lib/kibana/__mocks__/index.ts +++ b/x-pack/plugins/cases/public/common/lib/kibana/__mocks__/index.ts @@ -13,7 +13,7 @@ import { } from '../kibana_react.mock'; export const KibanaServices = { - get: jest.fn(), + get: jest.fn(() => ({})), getKibanaVersion: jest.fn(() => '8.0.0'), getConfig: jest.fn(() => null), }; diff --git a/x-pack/plugins/cases/public/common/lib/kibana/services.ts b/x-pack/plugins/cases/public/common/lib/kibana/services.ts index b1248488e5286..1846548c5b2b4 100644 --- a/x-pack/plugins/cases/public/common/lib/kibana/services.ts +++ b/x-pack/plugins/cases/public/common/lib/kibana/services.ts @@ -7,8 +7,10 @@ import type { CoreStart } from '@kbn/core/public'; import type { CasesUiConfigType } from '../../../../common/ui/types'; +import type { CasesPluginStart } from '../../../types'; -type GlobalServices = Pick; +type GlobalServices = Pick & + Pick; export class KibanaServices { private static kibanaVersion?: string; @@ -19,13 +21,14 @@ export class KibanaServices { application, config, http, + serverless, kibanaVersion, theme, }: GlobalServices & { kibanaVersion: string; config: CasesUiConfigType; }) { - this.services = { application, http, theme }; + this.services = { application, http, theme, serverless }; this.kibanaVersion = kibanaVersion; this.config = config; } diff --git a/x-pack/plugins/cases/public/components/all_cases/index.test.tsx b/x-pack/plugins/cases/public/components/all_cases/index.test.tsx index 7ce32a2f123a5..7661242fe9153 100644 --- a/x-pack/plugins/cases/public/components/all_cases/index.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/index.test.tsx @@ -20,6 +20,7 @@ import { useGetCurrentUserProfile } from '../../containers/user_profiles/use_get import { userProfiles, userProfilesMap } from '../../containers/user_profiles/api.mock'; import { useBulkGetUserProfiles } from '../../containers/user_profiles/use_bulk_get_user_profiles'; +jest.mock('../../common/lib/kibana'); jest.mock('../../containers/use_get_tags'); jest.mock('../../containers/use_get_action_license', () => { return { diff --git a/x-pack/plugins/cases/public/components/create/index.test.tsx b/x-pack/plugins/cases/public/components/create/index.test.tsx index 29f89226c61af..cb4b135212e0c 100644 --- a/x-pack/plugins/cases/public/components/create/index.test.tsx +++ b/x-pack/plugins/cases/public/components/create/index.test.tsx @@ -33,6 +33,7 @@ import { CreateCase } from '.'; import { useGetSupportedActionConnectors } from '../../containers/configure/use_get_supported_action_connectors'; import { useGetTags } from '../../containers/use_get_tags'; +jest.mock('../../common/lib/kibana'); jest.mock('../../containers/api'); jest.mock('../../containers/user_profiles/api'); jest.mock('../../containers/use_get_tags'); diff --git a/x-pack/plugins/cases/public/components/use_breadcrumbs/index.test.tsx b/x-pack/plugins/cases/public/components/use_breadcrumbs/index.test.tsx index 7a52686e64378..bdb726701adf6 100644 --- a/x-pack/plugins/cases/public/components/use_breadcrumbs/index.test.tsx +++ b/x-pack/plugins/cases/public/components/use_breadcrumbs/index.test.tsx @@ -14,11 +14,19 @@ import { CasesDeepLinkId } from '../../common/navigation'; const mockSetBreadcrumbs = jest.fn(); const mockSetTitle = jest.fn(); +const mockSetServerlessBreadcrumbs = jest.fn(); +const mockGetKibanaServices = jest.fn((): unknown => ({ + serverless: { setBreadcrumbs: mockSetServerlessBreadcrumbs }, +})); jest.mock('../../common/lib/kibana', () => { const originalModule = jest.requireActual('../../common/lib/kibana'); return { ...originalModule, + KibanaServices: { + ...originalModule.KibanaServices, + get: () => mockGetKibanaServices(), + }, useNavigation: jest.fn().mockReturnValue({ getAppUrl: jest.fn((params?: { deepLinkId: string }) => params?.deepLinkId ?? '/test'), }), @@ -50,12 +58,19 @@ describe('useCasesBreadcrumbs', () => { { href: '/test', onClick: expect.any(Function), text: 'Test' }, { text: 'Cases' }, ]); + expect(mockSetServerlessBreadcrumbs).toHaveBeenCalledWith([]); }); it('should sets the cases title', () => { renderHook(() => useCasesBreadcrumbs(CasesDeepLinkId.cases), { wrapper }); expect(mockSetTitle).toHaveBeenCalledWith(['Cases', 'Test']); }); + + it('should not set serverless breadcrumbs in ess', () => { + mockGetKibanaServices.mockReturnValueOnce({ serverless: undefined }); + renderHook(() => useCasesBreadcrumbs(CasesDeepLinkId.cases), { wrapper }); + expect(mockSetServerlessBreadcrumbs).not.toHaveBeenCalled(); + }); }); describe('set create_case breadcrumbs', () => { @@ -66,12 +81,19 @@ describe('useCasesBreadcrumbs', () => { { href: CasesDeepLinkId.cases, onClick: expect.any(Function), text: 'Cases' }, { text: 'Create' }, ]); + expect(mockSetServerlessBreadcrumbs).toHaveBeenCalledWith([]); }); it('should sets the cases title', () => { renderHook(() => useCasesBreadcrumbs(CasesDeepLinkId.casesCreate), { wrapper }); expect(mockSetTitle).toHaveBeenCalledWith(['Create', 'Cases', 'Test']); }); + + it('should not set serverless breadcrumbs in ess', () => { + mockGetKibanaServices.mockReturnValueOnce({ serverless: undefined }); + renderHook(() => useCasesBreadcrumbs(CasesDeepLinkId.casesCreate), { wrapper }); + expect(mockSetServerlessBreadcrumbs).not.toHaveBeenCalled(); + }); }); describe('set case_view breadcrumbs', () => { @@ -83,11 +105,18 @@ describe('useCasesBreadcrumbs', () => { { href: CasesDeepLinkId.cases, onClick: expect.any(Function), text: 'Cases' }, { text: title }, ]); + expect(mockSetServerlessBreadcrumbs).toHaveBeenCalledWith([{ text: title }]); }); it('should sets the cases title', () => { renderHook(() => useCasesTitleBreadcrumbs(title), { wrapper }); expect(mockSetTitle).toHaveBeenCalledWith([title, 'Cases', 'Test']); }); + + it('should not set serverless breadcrumbs in ess', () => { + mockGetKibanaServices.mockReturnValueOnce({ serverless: undefined }); + renderHook(() => useCasesTitleBreadcrumbs(title), { wrapper }); + expect(mockSetServerlessBreadcrumbs).not.toHaveBeenCalled(); + }); }); }); diff --git a/x-pack/plugins/cases/public/components/use_breadcrumbs/index.ts b/x-pack/plugins/cases/public/components/use_breadcrumbs/index.ts index 68a37f8252f05..24d1fa79e0d14 100644 --- a/x-pack/plugins/cases/public/components/use_breadcrumbs/index.ts +++ b/x-pack/plugins/cases/public/components/use_breadcrumbs/index.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import type { ChromeBreadcrumb } from '@kbn/core/public'; import { useCallback, useEffect } from 'react'; -import { useKibana, useNavigation } from '../../common/lib/kibana'; +import { KibanaServices, useKibana, useNavigation } from '../../common/lib/kibana'; import type { ICasesDeepLinkId } from '../../common/navigation'; import { CasesDeepLinkId } from '../../common/navigation'; import { useCasesContext } from '../cases_context/use_cases_context'; @@ -84,6 +84,7 @@ export const useCasesBreadcrumbs = (pageDeepLink: ICasesDeepLinkId) => { ] : []), ]); + KibanaServices.get().serverless?.setBreadcrumbs([]); }, [pageDeepLink, appTitle, getAppUrl, applyBreadcrumbs]); }; @@ -93,16 +94,18 @@ export const useCasesTitleBreadcrumbs = (caseTitle: string) => { const applyBreadcrumbs = useApplyBreadcrumbs(); useEffect(() => { + const titleBreadcrumb: ChromeBreadcrumb = { + text: caseTitle, + }; const casesBreadcrumbs: ChromeBreadcrumb[] = [ { text: appTitle, href: getAppUrl() }, { text: casesBreadcrumbTitle[CasesDeepLinkId.cases], href: getAppUrl({ deepLinkId: CasesDeepLinkId.cases }), }, - { - text: caseTitle, - }, + titleBreadcrumb, ]; applyBreadcrumbs(casesBreadcrumbs); + KibanaServices.get().serverless?.setBreadcrumbs([titleBreadcrumb]); }, [caseTitle, appTitle, getAppUrl, applyBreadcrumbs]); }; diff --git a/x-pack/plugins/cases/public/types.ts b/x-pack/plugins/cases/public/types.ts index 6206a3304c88d..c5a1fd8c73a69 100644 --- a/x-pack/plugins/cases/public/types.ts +++ b/x-pack/plugins/cases/public/types.ts @@ -25,6 +25,7 @@ import type { LicensingPluginStart } from '@kbn/licensing-plugin/public'; import type { FilesSetup, FilesStart } from '@kbn/files-plugin/public'; import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; import type { UiActionsStart } from '@kbn/ui-actions-plugin/public'; +import type { ServerlessPluginSetup, ServerlessPluginStart } from '@kbn/serverless/public'; import type { CasesBulkGetRequest, @@ -58,6 +59,7 @@ import type { PersistableStateAttachmentTypeRegistry } from './client/attachment export interface CasesPluginSetup { files: FilesSetup; security: SecurityPluginSetup; + serverless?: ServerlessPluginSetup; management: ManagementSetup; home?: HomePublicPluginSetup; } @@ -72,6 +74,7 @@ export interface CasesPluginStart { licensing?: LicensingPluginStart; savedObjectsManagement: SavedObjectsManagementPluginStart; security: SecurityPluginStart; + serverless?: ServerlessPluginStart; spaces?: SpacesPluginStart; storage: Storage; triggersActionsUi: TriggersActionsStart; diff --git a/x-pack/plugins/cases/tsconfig.json b/x-pack/plugins/cases/tsconfig.json index 4314e82ce6ba7..26e61c5c923aa 100644 --- a/x-pack/plugins/cases/tsconfig.json +++ b/x-pack/plugins/cases/tsconfig.json @@ -67,6 +67,7 @@ "@kbn/core-lifecycle-browser", "@kbn/core-saved-objects-api-server-mocks", "@kbn/core-theme-browser", + "@kbn/serverless", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/ess_security/public/breadcrumbs/breadcrumbs.test.ts b/x-pack/plugins/ess_security/public/breadcrumbs/breadcrumbs.test.ts new file mode 100644 index 0000000000000..3b4c340488572 --- /dev/null +++ b/x-pack/plugins/ess_security/public/breadcrumbs/breadcrumbs.test.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ChromeBreadcrumb } from '@kbn/core/public'; +import { emptyLastBreadcrumbUrl } from './breadcrumbs'; + +describe('emptyLastBreadcrumbUrl', () => { + it('should empty the URL and onClick function of the last breadcrumb', () => { + const breadcrumbs: ChromeBreadcrumb[] = [ + { text: 'Home', href: '/home', onClick: () => {} }, + { text: 'Breadcrumb 1', href: '/bc1', onClick: () => {} }, + { text: 'Last Breadcrumbs', href: '/last_bc', onClick: () => {} }, + ]; + + const expectedBreadcrumbs = [ + { text: 'Home', href: '/home', onClick: breadcrumbs[0].onClick }, + { text: 'Breadcrumb 1', href: '/bc1', onClick: breadcrumbs[1].onClick }, + { text: 'Last Breadcrumbs', href: '', onClick: undefined }, + ]; + + expect(emptyLastBreadcrumbUrl(breadcrumbs)).toEqual(expectedBreadcrumbs); + }); + + it('should return the original breadcrumbs if the input is empty', () => { + const emptyBreadcrumbs: ChromeBreadcrumb[] = []; + + expect(emptyLastBreadcrumbUrl(emptyBreadcrumbs)).toEqual(emptyBreadcrumbs); + }); +}); diff --git a/x-pack/plugins/ess_security/public/breadcrumbs/breadcrumbs.ts b/x-pack/plugins/ess_security/public/breadcrumbs/breadcrumbs.ts new file mode 100644 index 0000000000000..8fa8107226f4c --- /dev/null +++ b/x-pack/plugins/ess_security/public/breadcrumbs/breadcrumbs.ts @@ -0,0 +1,29 @@ +/* + * 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 type { PluginStart as SecuritySolutionPluginStart } from '@kbn/security-solution-plugin/public'; +import { ChromeBreadcrumb, CoreStart } from '@kbn/core/public'; + +export const subscribeBreadcrumbs = ( + securitySolution: SecuritySolutionPluginStart, + core: CoreStart +) => { + securitySolution.getBreadcrumbsNav$().subscribe((breadcrumbsNav) => { + const breadcrumbs = [...breadcrumbsNav.leading, ...breadcrumbsNav.trailing]; + if (breadcrumbs.length > 0) { + core.chrome.setBreadcrumbs(emptyLastBreadcrumbUrl(breadcrumbs)); + } + }); +}; + +export const emptyLastBreadcrumbUrl = (breadcrumbs: ChromeBreadcrumb[]) => { + const lastBreadcrumb = breadcrumbs[breadcrumbs.length - 1]; + if (lastBreadcrumb) { + return [...breadcrumbs.slice(0, -1), { ...lastBreadcrumb, href: '', onClick: undefined }]; + } + return breadcrumbs; +}; diff --git a/x-pack/plugins/ess_security/public/breadcrumbs/index.ts b/x-pack/plugins/ess_security/public/breadcrumbs/index.ts new file mode 100644 index 0000000000000..e1ed7fcff0986 --- /dev/null +++ b/x-pack/plugins/ess_security/public/breadcrumbs/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export { subscribeBreadcrumbs } from './breadcrumbs'; diff --git a/x-pack/plugins/ess_security/public/common/jest.config.js b/x-pack/plugins/ess_security/public/common/jest.config.js deleted file mode 100644 index ae6cd807e4cc1..0000000000000 --- a/x-pack/plugins/ess_security/public/common/jest.config.js +++ /dev/null @@ -1,26 +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. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../../../../..', - roots: ['/x-pack/plugins/ess_security/public/common'], - testMatch: ['/x-pack/plugins/ess_security/public/common/**/*.test.{js,mjs,ts,tsx}'], - coverageDirectory: - '/target/kibana-coverage/jest/x-pack/plugins/ess_security/public/common', - coverageReporters: ['text', 'html'], - collectCoverageFrom: [ - '/x-pack/plugins/ess_security/public/common/**/*.{ts,tsx}', - '!/x-pack/plugins/ess_security/public/common/*.test.{ts,tsx}', - '!/x-pack/plugins/ess_security/public/common/{__test__,__snapshots__,__examples__,*mock*,tests,test_helpers,integration_tests,types}/**/*', - '!/x-pack/plugins/ess_security/public/common/*mock*.{ts,tsx}', - '!/x-pack/plugins/ess_security/public/common/*.test.{ts,tsx}', - '!/x-pack/plugins/ess_security/public/common/*.d.ts', - '!/x-pack/plugins/ess_security/public/common/*.config.ts', - '!/x-pack/plugins/ess_security/public/common/index.{js,ts,tsx}', - ], -}; diff --git a/x-pack/plugins/ess_security/public/get_started/jest.config.js b/x-pack/plugins/ess_security/public/get_started/jest.config.js deleted file mode 100644 index 3dc5ab6b07f08..0000000000000 --- a/x-pack/plugins/ess_security/public/get_started/jest.config.js +++ /dev/null @@ -1,16 +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. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../../../../..', - roots: ['/x-pack/plugins/ess_security/public/get_started'], - coverageDirectory: - '/target/kibana-coverage/jest/x-pack/plugins/ess_security/public/get_started', - coverageReporters: ['text', 'html'], - collectCoverageFrom: ['/x-pack/plugins/ess_security/public/get_started/**/*.{ts,tsx}'], -}; diff --git a/x-pack/plugins/ess_security/public/jest.config.js b/x-pack/plugins/ess_security/public/jest.config.js index 2bdbadf77ba18..ffee6062ec59b 100644 --- a/x-pack/plugins/ess_security/public/jest.config.js +++ b/x-pack/plugins/ess_security/public/jest.config.js @@ -8,7 +8,7 @@ module.exports = { preset: '@kbn/test', rootDir: '../../../..', /** all nested directories have their own Jest config file */ - testMatch: ['/x-pack/plugins/ess_security/public/*.test.{js,mjs,ts,tsx}'], + testMatch: ['/x-pack/plugins/ess_security/public/**/*.test.{js,mjs,ts,tsx}'], roots: ['/x-pack/plugins/ess_security/public'], coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/ess_security/public', coverageReporters: ['text', 'html'], diff --git a/x-pack/plugins/ess_security/public/plugin.ts b/x-pack/plugins/ess_security/public/plugin.ts index a87744e00ffcd..52d75c01f8119 100644 --- a/x-pack/plugins/ess_security/public/plugin.ts +++ b/x-pack/plugins/ess_security/public/plugin.ts @@ -6,6 +6,7 @@ */ import { CoreSetup, CoreStart, Plugin } from '@kbn/core/public'; +import { subscribeBreadcrumbs } from './breadcrumbs'; import { getSecurityGetStartedComponent } from './get_started'; import { EssSecurityPluginSetup, @@ -26,8 +27,8 @@ export class EssSecurityPlugin constructor() {} public setup( - core: CoreSetup, - setupDeps: EssSecurityPluginSetupDependencies + _core: CoreSetup, + _setupDeps: EssSecurityPluginSetupDependencies ): EssSecurityPluginSetup { return {}; } @@ -37,6 +38,8 @@ export class EssSecurityPlugin startDeps: EssSecurityPluginStartDependencies ): EssSecurityPluginStart { const { securitySolution } = startDeps; + + subscribeBreadcrumbs(securitySolution, core); securitySolution.setGetStartedPage(getSecurityGetStartedComponent(core, startDeps)); return {}; diff --git a/x-pack/plugins/security_solution/public/cloud_security_posture/breadcrumbs.ts b/x-pack/plugins/security_solution/public/cloud_security_posture/breadcrumbs.ts index 16bd30db6680d..f02bc6f4ac32c 100644 --- a/x-pack/plugins/security_solution/public/cloud_security_posture/breadcrumbs.ts +++ b/x-pack/plugins/security_solution/public/cloud_security_posture/breadcrumbs.ts @@ -5,14 +5,14 @@ * 2.0. */ -import type { ChromeBreadcrumb } from '@kbn/core-chrome-browser'; -import type { GetSecuritySolutionUrl } from '../common/components/link_to'; -import type { RouteSpyState } from '../common/utils/route/types'; +import type { GetTrailingBreadcrumbs } from '../common/components/navigation/breadcrumbs/types'; -export const getTrailingBreadcrumbs = ( - params: RouteSpyState, - getSecuritySolutionUrl: GetSecuritySolutionUrl -): ChromeBreadcrumb[] => { +/** + * This module should only export this function. + * All the `getTrailingBreadcrumbs` functions in Security are loaded into the main bundle. + * We should be careful to not import unnecessary modules in this file to avoid increasing the main app bundle size. + */ +export const getTrailingBreadcrumbs: GetTrailingBreadcrumbs = (params, getSecuritySolutionUrl) => { const breadcrumbs = []; if (params.state?.ruleName) { diff --git a/x-pack/plugins/security_solution/public/common/breadcrumbs/breadcrumbs.ts b/x-pack/plugins/security_solution/public/common/breadcrumbs/breadcrumbs.ts new file mode 100644 index 0000000000000..8b61c7c2be10a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/breadcrumbs/breadcrumbs.ts @@ -0,0 +1,22 @@ +/* + * 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 { BehaviorSubject } from 'rxjs'; +import type { BreadcrumbsNav } from './types'; + +// Used to update the breadcrumbsNav$ observable internally. +const breadcrumbsNavUpdater$ = new BehaviorSubject({ + leading: [], + trailing: [], +}); + +// The observable can be exposed by the plugin contract. +export const breadcrumbsNav$ = breadcrumbsNavUpdater$.asObservable(); + +export const updateBreadcrumbsNav = (breadcrumbsNav: BreadcrumbsNav) => { + breadcrumbsNavUpdater$.next(breadcrumbsNav); +}; diff --git a/x-pack/plugins/security_solution/public/common/breadcrumbs/index.ts b/x-pack/plugins/security_solution/public/common/breadcrumbs/index.ts new file mode 100644 index 0000000000000..7caff04239860 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/breadcrumbs/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export type { BreadcrumbsNav } from './types'; +export { updateBreadcrumbsNav, breadcrumbsNav$ } from './breadcrumbs'; diff --git a/x-pack/plugins/security_solution/public/common/breadcrumbs/types.ts b/x-pack/plugins/security_solution/public/common/breadcrumbs/types.ts new file mode 100644 index 0000000000000..ab3aff52ec60c --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/breadcrumbs/types.ts @@ -0,0 +1,13 @@ +/* + * 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 type { ChromeBreadcrumb } from '@kbn/core/public'; + +export interface BreadcrumbsNav { + trailing: ChromeBreadcrumb[]; + leading: ChromeBreadcrumb[]; +} diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/get_breadcrumbs_for_page.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/get_breadcrumbs_for_page.ts deleted file mode 100644 index 325f490a351b1..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/get_breadcrumbs_for_page.ts +++ /dev/null @@ -1,33 +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 type { ChromeBreadcrumb } from '@kbn/core/public'; -import { SecurityPageName } from '../../../../app/types'; -import { APP_NAME } from '../../../../../common/constants'; -import { getAppLandingUrl } from '../../link_to/redirect_to_landing'; - -import type { GetSecuritySolutionUrl } from '../../link_to'; -import { getAncestorLinksInfo } from '../../../links'; - -export const getLeadingBreadcrumbsForSecurityPage = ( - pageName: SecurityPageName, - getSecuritySolutionUrl: GetSecuritySolutionUrl -): [ChromeBreadcrumb, ...ChromeBreadcrumb[]] => { - const landingPath = getSecuritySolutionUrl({ deepLinkId: SecurityPageName.landing }); - - const siemRootBreadcrumb: ChromeBreadcrumb = { - text: APP_NAME, - href: getAppLandingUrl(landingPath), - }; - - const breadcrumbs: ChromeBreadcrumb[] = getAncestorLinksInfo(pageName).map(({ title, id }) => ({ - text: title, - href: getSecuritySolutionUrl({ deepLinkId: id }), - })); - - return [siemRootBreadcrumb, ...breadcrumbs]; -}; 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 deleted file mode 100644 index 3d683bccd1b76..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts +++ /dev/null @@ -1,549 +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 '../../../mock/match_media'; -import { encodeIpv6 } from '../../../lib/helpers'; -import { getBreadcrumbsForRoute, useBreadcrumbs } from '.'; -import { HostsTableType } from '../../../../explore/hosts/store/model'; -import type { RouteSpyState } from '../../../utils/route/types'; -import { NetworkRouteType } from '../../../../explore/network/pages/navigation/types'; -import { AdministrationSubTab } from '../../../../management/types'; -import { renderHook } from '@testing-library/react-hooks'; -import { TestProviders } from '../../../mock'; -import type { GetSecuritySolutionUrl } from '../../link_to'; -import { APP_UI_ID, SecurityPageName } from '../../../../../common/constants'; -import { links } from '../../../links/app_links'; -import { updateAppLinks } from '../../../links'; -import { allowedExperimentalValues } from '../../../../../common/experimental_features'; -import { AlertDetailRouteType } from '../../../../detections/pages/alert_details/types'; -import { UsersTableType } from '../../../../explore/users/store/model'; -import { UpsellingService } from '../../../lib/upsellings'; - -const mockUseRouteSpy = jest.fn(); -jest.mock('../../../utils/route/use_route_spy', () => ({ - useRouteSpy: () => mockUseRouteSpy(), -})); - -const getMockObject = ( - pageName: SecurityPageName, - pathName: string, - detailName: string | undefined -): RouteSpyState => { - switch (pageName) { - case SecurityPageName.hosts: - return { - detailName, - pageName, - pathName, - search: '', - tabName: HostsTableType.authentications, - }; - - case SecurityPageName.users: - return { - detailName, - pageName, - pathName, - search: '', - tabName: UsersTableType.allUsers, - }; - - case SecurityPageName.network: - return { - detailName, - pageName, - pathName, - search: '', - tabName: NetworkRouteType.flows, - }; - - case SecurityPageName.administration: - return { - detailName, - pageName, - pathName, - search: '', - tabName: AdministrationSubTab.endpoints, - }; - - case SecurityPageName.alerts: - return { - detailName, - pageName, - pathName, - search: '', - tabName: AlertDetailRouteType.summary, - }; - - default: - return { - detailName, - pageName, - pathName, - search: '', - } as RouteSpyState; - } -}; - -// The string returned is different from what getSecuritySolutionUrl returns, but does not matter for the purposes of this test. -const getSecuritySolutionUrl: GetSecuritySolutionUrl = ({ - deepLinkId, - path, -}: { - deepLinkId?: string; - path?: string; - absolute?: boolean; -}) => `${APP_UI_ID}${deepLinkId ? `/${deepLinkId}` : ''}${path ?? ''}`; - -const mockSetBreadcrumbs = jest.fn(); -jest.mock('../../../lib/kibana/kibana_react', () => { - return { - useKibana: () => ({ - services: { - chrome: { - setBreadcrumbs: mockSetBreadcrumbs, - }, - application: { - navigateToApp: jest.fn(), - getUrlForApp: (appId: string, options?: { path?: string; deepLinkId?: boolean }) => - `${appId}/${options?.deepLinkId ?? ''}${options?.path ?? ''}`, - }, - }, - }), - }; -}); - -const hostName = 'siem-kibana'; - -const ipv4 = '192.0.2.255'; -const ipv6 = '2001:db8:ffff:ffff:ffff:ffff:ffff:ffff'; -const ipv6Encoded = encodeIpv6(ipv6); - -const securityBreadcrumb = { - href: 'securitySolutionUI/get_started', - text: 'Security', -}; - -const hostsBreadcrumb = { - href: 'securitySolutionUI/hosts', - text: 'Hosts', -}; - -const networkBreadcrumb = { - text: 'Network', - href: 'securitySolutionUI/network', -}; - -const exploreBreadcrumb = { - href: 'securitySolutionUI/explore', - text: 'Explore', -}; - -const rulesLandingBreadcrumb = { - text: 'Rules', - href: 'securitySolutionUI/rules-landing', -}; - -const rulesBreadcrumb = { - text: 'SIEM Rules', - href: 'securitySolutionUI/rules', -}; - -const exceptionsBreadcrumb = { - text: 'Shared Exception Lists', - href: 'securitySolutionUI/exceptions', -}; - -const settingsBreadcrumb = { - text: 'Settings', - href: 'securitySolutionUI/administration', -}; - -describe('Navigation Breadcrumbs', () => { - beforeAll(() => { - updateAppLinks(links, { - experimentalFeatures: allowedExperimentalValues, - capabilities: { - navLinks: {}, - management: {}, - catalogue: {}, - actions: { show: true, crud: true }, - siem: { - show: true, - crud: true, - }, - }, - upselling: new UpsellingService(), - }); - }); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('getBreadcrumbsForRoute', () => { - it('should return Overview breadcrumbs when supplied overview pageName', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject(SecurityPageName.overview, '/', undefined), - getSecuritySolutionUrl - ); - expect(breadcrumbs).toEqual([ - securityBreadcrumb, - { - href: 'securitySolutionUI/dashboards', - text: 'Dashboards', - }, - { - href: '', - text: 'Overview', - }, - ]); - }); - - it('should return Host breadcrumbs when supplied hosts pageName', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject(SecurityPageName.hosts, '/', undefined), - getSecuritySolutionUrl - ); - expect(breadcrumbs).toEqual([ - securityBreadcrumb, - exploreBreadcrumb, - hostsBreadcrumb, - { - href: '', - text: 'Authentications', - }, - ]); - }); - - it('should return Network breadcrumbs when supplied network pageName', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject(SecurityPageName.network, '/', undefined), - getSecuritySolutionUrl - ); - expect(breadcrumbs).toEqual([ - securityBreadcrumb, - exploreBreadcrumb, - networkBreadcrumb, - { - text: 'Flows', - href: '', - }, - ]); - }); - - it('should return Timelines breadcrumbs when supplied timelines pageName', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject(SecurityPageName.timelines, '/', undefined), - getSecuritySolutionUrl - ); - expect(breadcrumbs).toEqual([ - securityBreadcrumb, - { - text: 'Timelines', - href: '', - }, - ]); - }); - - it('should return Host Details breadcrumbs when supplied a pathname with hostName', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject(SecurityPageName.hosts, '/', hostName), - getSecuritySolutionUrl - ); - expect(breadcrumbs).toEqual([ - securityBreadcrumb, - exploreBreadcrumb, - hostsBreadcrumb, - { - text: 'siem-kibana', - href: 'securitySolutionUI/hosts/name/siem-kibana', - }, - { text: 'Authentications', href: '' }, - ]); - }); - - it('should return IP Details breadcrumbs when supplied pathname with ipv4', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject(SecurityPageName.network, '/', ipv4), - getSecuritySolutionUrl - ); - expect(breadcrumbs).toEqual([ - securityBreadcrumb, - exploreBreadcrumb, - networkBreadcrumb, - { - text: ipv4, - href: `securitySolutionUI/network/ip/${ipv4}/source/flows`, - }, - { text: 'Flows', href: '' }, - ]); - }); - - it('should return IP Details breadcrumbs when supplied pathname with ipv6', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject(SecurityPageName.network, '/', ipv6Encoded), - getSecuritySolutionUrl - ); - expect(breadcrumbs).toEqual([ - securityBreadcrumb, - exploreBreadcrumb, - networkBreadcrumb, - { - text: ipv6, - href: `securitySolutionUI/network/ip/${ipv6Encoded}/source/flows`, - }, - { text: 'Flows', href: '' }, - ]); - }); - - it('should return Alerts breadcrumbs when supplied alerts pageName', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject(SecurityPageName.alerts, '/alerts', undefined), - getSecuritySolutionUrl - ); - expect(breadcrumbs).toEqual([ - securityBreadcrumb, - { - text: 'Alerts', - href: 'securitySolutionUI/alerts', - }, - { - text: 'Summary', - href: '', - }, - ]); - }); - - it('should return Exceptions breadcrumbs when supplied exceptions pageName', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject(SecurityPageName.exceptions, '/exceptions', undefined), - getSecuritySolutionUrl - ); - expect(breadcrumbs).toEqual([ - securityBreadcrumb, - rulesLandingBreadcrumb, - { - text: 'Shared Exception Lists', - href: '', - }, - ]); - }); - - it('should return Rules breadcrumbs when supplied rules pageName', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject(SecurityPageName.rules, '/rules', undefined), - getSecuritySolutionUrl - ); - expect(breadcrumbs).toEqual([ - securityBreadcrumb, - rulesLandingBreadcrumb, - { - ...rulesBreadcrumb, - href: '', - }, - ]); - }); - - it('should return Rules breadcrumbs when supplied rules Creation pageName', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject(SecurityPageName.rules, '/rules/create', undefined), - getSecuritySolutionUrl - ); - expect(breadcrumbs).toEqual([ - securityBreadcrumb, - rulesLandingBreadcrumb, - rulesBreadcrumb, - { - text: 'Create', - href: '', - }, - ]); - }); - - it('should return Rules breadcrumbs when supplied rules Details pageName', () => { - const mockDetailName = '5a4a0460-d822-11eb-8962-bfd4aff0a9b3'; - const mockRuleName = 'ALERT_RULE_NAME'; - const breadcrumbs = getBreadcrumbsForRoute( - { - ...getMockObject(SecurityPageName.rules, `/rules/id/${mockDetailName}`, undefined), - detailName: mockDetailName, - state: { - ruleName: mockRuleName, - }, - }, - getSecuritySolutionUrl - ); - expect(breadcrumbs).toEqual([ - securityBreadcrumb, - rulesLandingBreadcrumb, - rulesBreadcrumb, - { - text: mockRuleName, - href: `securitySolutionUI/rules/id/${mockDetailName}`, - }, - { - text: 'Deleted rule', - href: '', - }, - ]); - }); - - it('should return Rules breadcrumbs when supplied rules Edit pageName', () => { - const mockDetailName = '5a4a0460-d822-11eb-8962-bfd4aff0a9b3'; - const mockRuleName = 'ALERT_RULE_NAME'; - const breadcrumbs = getBreadcrumbsForRoute( - { - ...getMockObject(SecurityPageName.rules, `/rules/id/${mockDetailName}/edit`, undefined), - detailName: mockDetailName, - state: { - ruleName: mockRuleName, - }, - }, - getSecuritySolutionUrl - ); - expect(breadcrumbs).toEqual([ - securityBreadcrumb, - rulesLandingBreadcrumb, - rulesBreadcrumb, - { - text: 'ALERT_RULE_NAME', - href: `securitySolutionUI/rules/id/${mockDetailName}`, - }, - { - text: 'Edit', - href: '', - }, - ]); - }); - - it('should return null breadcrumbs when supplied Cases pageName', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject(SecurityPageName.case, '/', undefined), - getSecuritySolutionUrl - ); - expect(breadcrumbs).toEqual(null); - }); - - it('should return null breadcrumbs when supplied Cases details pageName', () => { - const sampleCase = { - id: 'my-case-id', - name: 'Case name', - }; - const breadcrumbs = getBreadcrumbsForRoute( - { - ...getMockObject(SecurityPageName.case, `/${sampleCase.id}`, sampleCase.id), - state: { caseTitle: sampleCase.name }, - }, - getSecuritySolutionUrl - ); - expect(breadcrumbs).toEqual(null); - }); - - it('should return Endpoints breadcrumbs when supplied endpoints pageName', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject(SecurityPageName.endpoints, '/', undefined), - getSecuritySolutionUrl - ); - - expect(breadcrumbs).toEqual([ - securityBreadcrumb, - settingsBreadcrumb, - { - text: 'Endpoints', - href: '', - }, - ]); - }); - - it('should return Admin breadcrumbs when supplied admin pageName', () => { - const breadcrumbs = getBreadcrumbsForRoute( - getMockObject(SecurityPageName.administration, '/', undefined), - getSecuritySolutionUrl - ); - - expect(breadcrumbs).toEqual([ - securityBreadcrumb, - { - ...settingsBreadcrumb, - href: '', - }, - ]); - }); - it('should return Exceptions breadcrumbs when supplied exception Details pageName', () => { - const mockListName = 'new shared list'; - const breadcrumbs = getBreadcrumbsForRoute( - { - ...getMockObject( - SecurityPageName.exceptions, - `/exceptions/details/${mockListName}`, - undefined - ), - state: { - listName: mockListName, - }, - }, - getSecuritySolutionUrl - ); - expect(breadcrumbs).toEqual([ - securityBreadcrumb, - rulesLandingBreadcrumb, - exceptionsBreadcrumb, - { - text: mockListName, - href: ``, - }, - ]); - }); - }); - - describe('setBreadcrumbs()', () => { - it('should call chrome breadcrumb service with correct breadcrumbs', () => { - mockUseRouteSpy.mockReturnValueOnce([getMockObject(SecurityPageName.hosts, '/', hostName)]); - renderHook(useBreadcrumbs, { - initialProps: { isEnabled: true }, - wrapper: TestProviders, - }); - - expect(mockSetBreadcrumbs).toHaveBeenCalledWith([ - expect.objectContaining({ - text: 'Security', - href: 'securitySolutionUI/get_started', - onClick: expect.any(Function), - }), - expect.objectContaining({ - text: 'Explore', - href: 'securitySolutionUI/explore', - onClick: expect.any(Function), - }), - expect.objectContaining({ - text: 'Hosts', - href: 'securitySolutionUI/hosts', - onClick: expect.any(Function), - }), - expect.objectContaining({ - text: 'siem-kibana', - href: 'securitySolutionUI/hosts/name/siem-kibana', - onClick: expect.any(Function), - }), - { - text: 'Authentications', - href: '', - }, - ]); - }); - - it('should not call chrome breadcrumb service when not enabled', () => { - mockUseRouteSpy.mockReturnValueOnce([getMockObject(SecurityPageName.hosts, '/', hostName)]); - renderHook(useBreadcrumbs, { - initialProps: { isEnabled: false }, - wrapper: TestProviders, - }); - expect(mockSetBreadcrumbs).not.toHaveBeenCalled(); - }); - }); -}); 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 cfbffae6ad8de..819b50bfb48be 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 @@ -4,136 +4,4 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { useEffect } from 'react'; -import { last } from 'lodash/fp'; -import { useDispatch } from 'react-redux'; -import type { ChromeBreadcrumb } from '@kbn/core/public'; -import { METRIC_TYPE } from '@kbn/analytics'; -import { getTrailingBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../../explore/hosts/pages/details/utils'; -import { getTrailingBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../../explore/network/pages/details/utils'; -import { getTrailingBreadcrumbs as getDetectionRulesBreadcrumbs } from '../../../../detections/pages/detection_engine/rules/utils'; -import { getTrailingBreadcrumbs as geExceptionsBreadcrumbs } from '../../../../exceptions/utils/pages.utils'; -import { getTrailingBreadcrumbs as getCSPBreadcrumbs } from '../../../../cloud_security_posture/breadcrumbs'; -import { getTrailingBreadcrumbs as getUsersBreadcrumbs } from '../../../../explore/users/pages/details/utils'; -import { getTrailingBreadcrumbs as getKubernetesBreadcrumbs } from '../../../../kubernetes/pages/utils/breadcrumbs'; -import { getTrailingBreadcrumbs as getAlertDetailBreadcrumbs } from '../../../../detections/pages/alert_details/utils/breadcrumbs'; -import { getTrailingBreadcrumbs as getDashboardBreadcrumbs } from '../../../../dashboards/pages/utils'; -import { SecurityPageName } from '../../../../app/types'; -import type { RouteSpyState } from '../../../utils/route/types'; -import { timelineActions } from '../../../../timelines/store/timeline'; -import { TimelineId } from '../../../../../common/types/timeline'; -import { getLeadingBreadcrumbsForSecurityPage } from './get_breadcrumbs_for_page'; -import type { GetSecuritySolutionUrl } from '../../link_to'; -import { useGetSecuritySolutionUrl } from '../../link_to'; -import { TELEMETRY_EVENT, track } from '../../../lib/telemetry'; -import { useKibana } from '../../../lib/kibana'; -import { useRouteSpy } from '../../../utils/route/use_route_spy'; - -export const useBreadcrumbs = ({ isEnabled }: { isEnabled: boolean }) => { - const dispatch = useDispatch(); - const [routeProps] = useRouteSpy(); - const getSecuritySolutionUrl = useGetSecuritySolutionUrl(); - const { - chrome: { setBreadcrumbs }, - application: { navigateToUrl }, - } = useKibana().services; - - useEffect(() => { - if (!isEnabled) { - return; - } - const breadcrumbs = getBreadcrumbsForRoute(routeProps, getSecuritySolutionUrl); - if (!breadcrumbs) { - return; - } - setBreadcrumbs( - breadcrumbs.map((breadcrumb) => ({ - ...breadcrumb, - ...(breadcrumb.href && !breadcrumb.onClick - ? { - onClick: (ev) => { - ev.preventDefault(); - const trackedPath = breadcrumb.href?.split('?')[0] ?? 'unknown'; - track(METRIC_TYPE.CLICK, `${TELEMETRY_EVENT.BREADCRUMB}${trackedPath}`); - dispatch(timelineActions.showTimeline({ id: TimelineId.active, show: false })); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - navigateToUrl(breadcrumb.href!); - }, - } - : {}), - })) - ); - }, [routeProps, isEnabled, dispatch, getSecuritySolutionUrl, setBreadcrumbs, navigateToUrl]); -}; - -export const getBreadcrumbsForRoute = ( - spyState: RouteSpyState, - getSecuritySolutionUrl: GetSecuritySolutionUrl -): ChromeBreadcrumb[] | null => { - if ( - !spyState?.pageName || - // cases manages its own breadcrumbs, return null - spyState.pageName === SecurityPageName.case - ) { - return null; - } - - const leadingBreadcrumbs = getLeadingBreadcrumbsForSecurityPage( - spyState.pageName, - getSecuritySolutionUrl - ); - - return emptyLastBreadcrumbUrl([ - ...leadingBreadcrumbs, - ...getTrailingBreadcrumbsForRoutes(spyState, getSecuritySolutionUrl), - ]); -}; - -const getTrailingBreadcrumbsForRoutes = ( - spyState: RouteSpyState, - getSecuritySolutionUrl: GetSecuritySolutionUrl -): ChromeBreadcrumb[] => { - switch (spyState.pageName) { - case SecurityPageName.hosts: - return getHostDetailsBreadcrumbs(spyState, getSecuritySolutionUrl); - case SecurityPageName.network: - return getIPDetailsBreadcrumbs(spyState, getSecuritySolutionUrl); - case SecurityPageName.users: - return getUsersBreadcrumbs(spyState, getSecuritySolutionUrl); - case SecurityPageName.rules: - case SecurityPageName.rulesAdd: - case SecurityPageName.rulesCreate: - return getDetectionRulesBreadcrumbs(spyState, getSecuritySolutionUrl); - case SecurityPageName.exceptions: - return geExceptionsBreadcrumbs(spyState, getSecuritySolutionUrl); - case SecurityPageName.kubernetes: - return getKubernetesBreadcrumbs(spyState, getSecuritySolutionUrl); - case SecurityPageName.alerts: - return getAlertDetailBreadcrumbs(spyState, getSecuritySolutionUrl); - case SecurityPageName.cloudSecurityPostureBenchmarks: - return getCSPBreadcrumbs(spyState, getSecuritySolutionUrl); - case SecurityPageName.dashboards: - return getDashboardBreadcrumbs(spyState); - } - - return []; -}; - -const emptyLastBreadcrumbUrl = (breadcrumbs: ChromeBreadcrumb[]) => { - const leadingBreadCrumbs = breadcrumbs.slice(0, -1); - const lastBreadcrumb = last(breadcrumbs); - - if (lastBreadcrumb) { - return [ - ...leadingBreadCrumbs, - { - ...lastBreadcrumb, - href: '', - }, - ]; - } - - return breadcrumbs; -}; +export { useBreadcrumbsNav } from './use_breadcrumbs_nav'; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/trailing_breadcrumbs.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/trailing_breadcrumbs.ts new file mode 100644 index 0000000000000..5c45da1bb1ff2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/trailing_breadcrumbs.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SecurityPageName } from '../../../../../common'; +import type { GetTrailingBreadcrumbs } from './types'; + +import { getTrailingBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../../explore/hosts/pages/details/breadcrumbs'; +import { getTrailingBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../../explore/network/pages/details/breadcrumbs'; +import { getTrailingBreadcrumbs as getDetectionRulesBreadcrumbs } from '../../../../detections/pages/detection_engine/rules/breadcrumbs'; +import { getTrailingBreadcrumbs as geExceptionsBreadcrumbs } from '../../../../exceptions/utils/breadcrumbs'; +import { getTrailingBreadcrumbs as getCSPBreadcrumbs } from '../../../../cloud_security_posture/breadcrumbs'; +import { getTrailingBreadcrumbs as getUsersBreadcrumbs } from '../../../../explore/users/pages/details/breadcrumbs'; +import { getTrailingBreadcrumbs as getKubernetesBreadcrumbs } from '../../../../kubernetes/pages/utils/breadcrumbs'; +import { getTrailingBreadcrumbs as getAlertDetailBreadcrumbs } from '../../../../detections/pages/alert_details/utils/breadcrumbs'; +import { getTrailingBreadcrumbs as getDashboardBreadcrumbs } from '../../../../dashboards/pages/breadcrumbs'; + +export const getTrailingBreadcrumbs: GetTrailingBreadcrumbs = ( + spyState, + getSecuritySolutionUrl +) => { + switch (spyState.pageName) { + case SecurityPageName.hosts: + return getHostDetailsBreadcrumbs(spyState, getSecuritySolutionUrl); + case SecurityPageName.network: + return getIPDetailsBreadcrumbs(spyState, getSecuritySolutionUrl); + case SecurityPageName.users: + return getUsersBreadcrumbs(spyState, getSecuritySolutionUrl); + case SecurityPageName.rules: + case SecurityPageName.rulesAdd: + case SecurityPageName.rulesCreate: + return getDetectionRulesBreadcrumbs(spyState, getSecuritySolutionUrl); + case SecurityPageName.exceptions: + return geExceptionsBreadcrumbs(spyState, getSecuritySolutionUrl); + case SecurityPageName.kubernetes: + return getKubernetesBreadcrumbs(spyState, getSecuritySolutionUrl); + case SecurityPageName.alerts: + return getAlertDetailBreadcrumbs(spyState, getSecuritySolutionUrl); + case SecurityPageName.cloudSecurityPostureBenchmarks: + return getCSPBreadcrumbs(spyState, getSecuritySolutionUrl); + case SecurityPageName.dashboards: + return getDashboardBreadcrumbs(spyState, getSecuritySolutionUrl); + } + return []; +}; diff --git a/x-pack/plugins/security_solution/public/dashboards/pages/utils.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/types.ts similarity index 51% rename from x-pack/plugins/security_solution/public/dashboards/pages/utils.ts rename to x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/types.ts index c71eedbec0264..9efa8320b1fd7 100644 --- a/x-pack/plugins/security_solution/public/dashboards/pages/utils.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/types.ts @@ -6,13 +6,10 @@ */ import type { ChromeBreadcrumb } from '@kbn/core/public'; -import type { RouteSpyState } from '../../common/utils/route/types'; +import type { RouteSpyState } from '../../../utils/route/types'; +import type { GetSecuritySolutionUrl } from '../../link_to'; -export const getTrailingBreadcrumbs = (params: RouteSpyState): ChromeBreadcrumb[] => { - const breadcrumbName = params?.state?.dashboardName; - if (breadcrumbName) { - return [{ text: breadcrumbName }]; - } - - return []; -}; +export type GetTrailingBreadcrumbs = ( + spyState: T, + getSecuritySolutionUrl: GetSecuritySolutionUrl +) => ChromeBreadcrumb[]; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/use_breadcrumbs_nav.test.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/use_breadcrumbs_nav.test.ts new file mode 100644 index 0000000000000..25c29fdb9fa2d --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/use_breadcrumbs_nav.test.ts @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import type { ChromeBreadcrumb } from '@kbn/core/public'; +import type { GetSecuritySolutionUrl } from '../../link_to'; +import { SecurityPageName } from '../../../../../common/constants'; +import type { LinkInfo, LinkItem } from '../../../links'; +import { useBreadcrumbsNav } from './use_breadcrumbs_nav'; +import type { BreadcrumbsNav } from '../../../breadcrumbs'; + +jest.mock('../../../lib/kibana'); + +const mockDispatch = jest.fn(); +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useDispatch: () => mockDispatch, +})); + +const link1Id = 'link-1' as SecurityPageName; +const link2Id = 'link-2' as SecurityPageName; +const link3Id = 'link-3' as SecurityPageName; +const link4Id = 'link-4' as SecurityPageName; +const link5Id = 'link-5' as SecurityPageName; + +const link1: LinkItem = { id: link1Id, title: 'link 1', path: '/link1' }; +const link2: LinkItem = { id: link2Id, title: 'link 2', path: '/link2' }; +const link3: LinkItem = { id: link3Id, title: 'link 3', path: '/link3' }; +const link4: LinkItem = { id: link4Id, title: 'link 4', path: '/link4' }; +const link5: LinkItem = { id: link5Id, title: 'link 5', path: '/link5' }; + +const ancestorsLinks = [link1, link2, link3]; +const trailingLinks = [link4, link5]; +const allLinks = [...ancestorsLinks, ...trailingLinks]; + +const mockSecuritySolutionUrl: GetSecuritySolutionUrl = jest.fn( + ({ deepLinkId }: { deepLinkId: SecurityPageName }) => + allLinks.find((link) => link.id === deepLinkId)?.path ?? deepLinkId +); +jest.mock('../../link_to', () => ({ + useGetSecuritySolutionUrl: () => mockSecuritySolutionUrl, +})); + +const mockUpdateBreadcrumbsNav = jest.fn((_param: BreadcrumbsNav) => {}); +jest.mock('../../../breadcrumbs', () => ({ + updateBreadcrumbsNav: (param: BreadcrumbsNav) => mockUpdateBreadcrumbsNav(param), +})); + +const mockUseRouteSpy = jest.fn((): [{ pageName: string }] => [{ pageName: link1Id }]); +jest.mock('../../../utils/route/use_route_spy', () => ({ + useRouteSpy: () => mockUseRouteSpy(), +})); + +const mockGetAncestorLinks = jest.fn((_id: unknown): LinkInfo[] => ancestorsLinks); +jest.mock('../../../links', () => ({ + ...jest.requireActual('../../../links'), + getAncestorLinksInfo: (id: unknown) => mockGetAncestorLinks(id), +})); + +const mockGetTrailingBreadcrumbs = jest.fn((): ChromeBreadcrumb[] => + trailingLinks.map(({ title: text, path: href }) => ({ text, href })) +); +jest.mock('./trailing_breadcrumbs', () => ({ + getTrailingBreadcrumbs: () => mockGetTrailingBreadcrumbs(), +})); + +const landingBreadcrumb = { + href: 'get_started', + text: 'Security', + onClick: expect.any(Function), +}; + +describe('useBreadcrumbsNav', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should process breadcrumbs with current pageName', () => { + renderHook(useBreadcrumbsNav); + expect(mockGetAncestorLinks).toHaveBeenCalledWith(link1Id); + expect(mockGetTrailingBreadcrumbs).toHaveBeenCalledWith(); + }); + + it('should not process breadcrumbs with empty pageName', () => { + mockUseRouteSpy.mockReturnValueOnce([{ pageName: '' }]); + renderHook(useBreadcrumbsNav); + expect(mockGetAncestorLinks).not.toHaveBeenCalled(); + expect(mockGetTrailingBreadcrumbs).not.toHaveBeenCalledWith(); + }); + + it('should not process breadcrumbs with cases pageName', () => { + mockUseRouteSpy.mockReturnValueOnce([{ pageName: SecurityPageName.case }]); + renderHook(useBreadcrumbsNav); + expect(mockGetAncestorLinks).not.toHaveBeenCalled(); + expect(mockGetTrailingBreadcrumbs).not.toHaveBeenCalledWith(); + }); + + it('should call updateBreadcrumbsNav with all breadcrumbs', () => { + renderHook(useBreadcrumbsNav); + expect(mockUpdateBreadcrumbsNav).toHaveBeenCalledWith({ + leading: [ + landingBreadcrumb, + { + href: link1.path, + text: link1.title, + onClick: expect.any(Function), + }, + { + href: link2.path, + text: link2.title, + onClick: expect.any(Function), + }, + { + href: link3.path, + text: link3.title, + onClick: expect.any(Function), + }, + ], + trailing: [ + { + href: link4.path, + text: link4.title, + onClick: expect.any(Function), + }, + { + href: link5.path, + text: link5.title, + onClick: expect.any(Function), + }, + ], + }); + }); + + it('should create breadcrumbs onClick handler', () => { + renderHook(useBreadcrumbsNav); + const event = { preventDefault: jest.fn() } as unknown as React.MouseEvent< + HTMLElement, + MouseEvent + >; + const breadcrumb = mockUpdateBreadcrumbsNav.mock.calls?.[0]?.[0]?.leading[1]; + breadcrumb?.onClick?.(event); + + expect(event.preventDefault).toHaveBeenCalled(); + expect(mockDispatch).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/use_breadcrumbs_nav.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/use_breadcrumbs_nav.ts new file mode 100644 index 0000000000000..9eeae743bffaa --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/use_breadcrumbs_nav.ts @@ -0,0 +1,87 @@ +/* + * 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 { useEffect } from 'react'; +import { useDispatch } from 'react-redux'; +import type { ChromeBreadcrumb } from '@kbn/core/public'; +import { METRIC_TYPE } from '@kbn/analytics'; +import type { Dispatch } from 'redux'; +import { SecurityPageName } from '../../../../app/types'; +import type { RouteSpyState } from '../../../utils/route/types'; +import { timelineActions } from '../../../../timelines/store/timeline'; +import { TimelineId } from '../../../../../common/types/timeline'; +import type { GetSecuritySolutionUrl } from '../../link_to'; +import { useGetSecuritySolutionUrl } from '../../link_to'; +import { TELEMETRY_EVENT, track } from '../../../lib/telemetry'; +import { useNavigateTo, type NavigateTo } from '../../../lib/kibana'; +import { useRouteSpy } from '../../../utils/route/use_route_spy'; +import { updateBreadcrumbsNav } from '../../../breadcrumbs'; +import { getAncestorLinksInfo } from '../../../links'; +import { APP_NAME } from '../../../../../common/constants'; +import { getTrailingBreadcrumbs } from './trailing_breadcrumbs'; + +export const useBreadcrumbsNav = () => { + const dispatch = useDispatch(); + const [routeProps] = useRouteSpy(); + const { navigateTo } = useNavigateTo(); + const getSecuritySolutionUrl = useGetSecuritySolutionUrl(); + + useEffect(() => { + // cases manages its own breadcrumbs + if (!routeProps.pageName || routeProps.pageName === SecurityPageName.case) { + return; + } + + const leadingBreadcrumbs = getLeadingBreadcrumbs(routeProps, getSecuritySolutionUrl); + const trailingBreadcrumbs = getTrailingBreadcrumbs(routeProps, getSecuritySolutionUrl); + + updateBreadcrumbsNav({ + leading: addOnClicksHandlers(leadingBreadcrumbs, dispatch, navigateTo), + trailing: addOnClicksHandlers(trailingBreadcrumbs, dispatch, navigateTo), + }); + }, [routeProps, getSecuritySolutionUrl, dispatch, navigateTo]); +}; + +const getLeadingBreadcrumbs = ( + { pageName }: RouteSpyState, + getSecuritySolutionUrl: GetSecuritySolutionUrl +): ChromeBreadcrumb[] => { + const landingBreadcrumb: ChromeBreadcrumb = { + text: APP_NAME, + href: getSecuritySolutionUrl({ deepLinkId: SecurityPageName.landing }), + }; + + const breadcrumbs: ChromeBreadcrumb[] = getAncestorLinksInfo(pageName).map(({ title, id }) => ({ + text: title, + href: getSecuritySolutionUrl({ deepLinkId: id }), + })); + + return [landingBreadcrumb, ...breadcrumbs]; +}; + +const addOnClicksHandlers = ( + breadcrumbs: ChromeBreadcrumb[], + dispatch: Dispatch, + navigateTo: NavigateTo +): ChromeBreadcrumb[] => + breadcrumbs.map((breadcrumb) => ({ + ...breadcrumb, + ...(breadcrumb.href && + !breadcrumb.onClick && { + onClick: createOnClickHandler(breadcrumb.href, dispatch, navigateTo), + }), + })); + +const createOnClickHandler = + (href: string, dispatch: Dispatch, navigateTo: NavigateTo): ChromeBreadcrumb['onClick'] => + (ev) => { + ev.preventDefault(); + const trackedPath = href.split('?')[0]; + track(METRIC_TYPE.CLICK, `${TELEMETRY_EVENT.BREADCRUMB}${trackedPath}`); + dispatch(timelineActions.showTimeline({ id: TimelineId.active, show: false })); + navigateTo({ url: href }); + }; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_security_solution_navigation.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_security_solution_navigation.test.tsx index 42f46525bdbe4..f1d94adc79a1b 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_security_solution_navigation.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_security_solution_navigation.test.tsx @@ -9,9 +9,8 @@ import { renderHook } from '@testing-library/react-hooks'; import { BehaviorSubject } from 'rxjs'; import { useSecuritySolutionNavigation } from './use_security_solution_navigation'; -const mockSetBreadcrumbs = jest.fn(); jest.mock('../breadcrumbs', () => ({ - useBreadcrumbs: () => mockSetBreadcrumbs, + useBreadcrumbsNav: () => jest.fn(), })); const mockIsSidebarEnabled$ = new BehaviorSubject(true); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_security_solution_navigation.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_security_solution_navigation.tsx index bf4e8359cb464..315a73a950edf 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_security_solution_navigation.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_security_solution_navigation.tsx @@ -16,7 +16,7 @@ import useObservable from 'react-use/lib/useObservable'; import { i18n } from '@kbn/i18n'; import type { KibanaPageTemplateProps } from '@kbn/shared-ux-page-kibana-template'; import { useKibana } from '../../../lib/kibana'; -import { useBreadcrumbs } from '../breadcrumbs'; +import { useBreadcrumbsNav } from '../breadcrumbs'; import { SecuritySideNav } from '../security_side_nav'; const translatedNavTitle = i18n.translate('xpack.securitySolution.navigation.mainLabel', { @@ -27,9 +27,7 @@ export const useSecuritySolutionNavigation = (): KibanaPageTemplateProps['soluti const { isSidebarEnabled$ } = useKibana().services; const isSidebarEnabled = useObservable(isSidebarEnabled$); - useBreadcrumbs({ - isEnabled: true, // TODO: use isSidebarEnabled$ when serverless breadcrumb is ready - }); + useBreadcrumbsNav(); if (!isSidebarEnabled) { return undefined; diff --git a/x-pack/plugins/security_solution/public/dashboards/pages/breadcrumbs.ts b/x-pack/plugins/security_solution/public/dashboards/pages/breadcrumbs.ts new file mode 100644 index 0000000000000..01e663e8abb7e --- /dev/null +++ b/x-pack/plugins/security_solution/public/dashboards/pages/breadcrumbs.ts @@ -0,0 +1,22 @@ +/* + * 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 type { GetTrailingBreadcrumbs } from '../../common/components/navigation/breadcrumbs/types'; + +/** + * This module should only export this function. + * All the `getTrailingBreadcrumbs` functions in Security are loaded into the main bundle. + * We should be careful to not import unnecessary modules in this file to avoid increasing the main app bundle size. + */ +export const getTrailingBreadcrumbs: GetTrailingBreadcrumbs = (params, getSecuritySolutionUrl) => { + const breadcrumbName = params?.state?.dashboardName; + if (breadcrumbName) { + return [{ text: breadcrumbName }]; + } + + return []; +}; diff --git a/x-pack/plugins/security_solution/public/detections/pages/alert_details/utils/breadcrumbs.ts b/x-pack/plugins/security_solution/public/detections/pages/alert_details/utils/breadcrumbs.ts index 632c40816476b..2b6dca72bf078 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/alert_details/utils/breadcrumbs.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/alert_details/utils/breadcrumbs.ts @@ -6,7 +6,7 @@ */ import type { ChromeBreadcrumb } from '@kbn/core/public'; -import type { GetSecuritySolutionUrl } from '../../../../common/components/link_to'; +import type { GetTrailingBreadcrumbs } from '../../../../common/components/navigation/breadcrumbs/types'; import { getAlertDetailsUrl } from '../../../../common/components/link_to'; import { SecurityPageName } from '../../../../../common/constants'; import type { AlertDetailRouteSpyState } from '../../../../common/utils/route/types'; @@ -17,10 +17,15 @@ const TabNameMappedToI18nKey: Record = { [AlertDetailRouteType.summary]: i18n.SUMMARY_PAGE_TITLE, }; -export const getTrailingBreadcrumbs = ( - params: AlertDetailRouteSpyState, - getSecuritySolutionUrl: GetSecuritySolutionUrl -): ChromeBreadcrumb[] => { +/** + * This module should only export this function. + * All the `getTrailingBreadcrumbs` functions in Security are loaded into the main bundle. + * We should be careful to not import unnecessary modules in this file to avoid increasing the main app bundle size. + */ +export const getTrailingBreadcrumbs: GetTrailingBreadcrumbs = ( + params, + getSecuritySolutionUrl +) => { let breadcrumb: ChromeBreadcrumb[] = []; if (params.detailName != null) { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/breadcrumbs.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/breadcrumbs.ts new file mode 100644 index 0000000000000..0bd84ee4724ce --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/breadcrumbs.ts @@ -0,0 +1,92 @@ +/* + * 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 type { ChromeBreadcrumb } from '@kbn/core/public'; +import { + getRuleDetailsTabUrl, + getRuleDetailsUrl, +} from '../../../../common/components/link_to/redirect_to_detection_engine'; +import * as i18nRules from './translations'; +import { SecurityPageName } from '../../../../app/types'; +import { RULES_PATH } from '../../../../../common/constants'; +import type { GetTrailingBreadcrumbs } from '../../../../common/components/navigation/breadcrumbs/types'; +import { + RuleDetailTabs, + RULE_DETAILS_TAB_NAME, +} from '../../../../detection_engine/rule_details_ui/pages/rule_details'; +import { DELETED_RULE } from '../../../../detection_engine/rule_details_ui/pages/rule_details/translations'; + +const getRuleDetailsTabName = (tabName: string): string => { + return RULE_DETAILS_TAB_NAME[tabName] ?? RULE_DETAILS_TAB_NAME[RuleDetailTabs.alerts]; +}; + +const isRuleCreatePage = (pathname: string) => + pathname.includes(RULES_PATH) && pathname.includes('/create'); + +const isRuleEditPage = (pathname: string) => + pathname.includes(RULES_PATH) && pathname.includes('/edit'); + +/** + * This module should only export this function. + * All the `getTrailingBreadcrumbs` functions in Security are loaded into the main bundle. + * We should be careful to not import unnecessary modules in this file to avoid increasing the main app bundle size. + */ +export const getTrailingBreadcrumbs: GetTrailingBreadcrumbs = (params, getSecuritySolutionUrl) => { + let breadcrumb: ChromeBreadcrumb[] = []; + + if (params.detailName && params.state?.ruleName) { + breadcrumb = [ + ...breadcrumb, + { + text: params.state.ruleName, + href: getSecuritySolutionUrl({ + deepLinkId: SecurityPageName.rules, + path: getRuleDetailsUrl(params.detailName, ''), + }), + }, + ]; + } + + if (params.detailName && params.state?.ruleName && params.tabName) { + breadcrumb = [ + ...breadcrumb, + { + text: getRuleDetailsTabName(params.tabName), + href: getSecuritySolutionUrl({ + deepLinkId: SecurityPageName.rules, + path: getRuleDetailsTabUrl(params.detailName, params.tabName, ''), + }), + }, + ]; + } + + if (isRuleCreatePage(params.pathName)) { + breadcrumb = [ + ...breadcrumb, + { + text: i18nRules.ADD_PAGE_TITLE, + href: '', + }, + ]; + } + + if (isRuleEditPage(params.pathName) && params.detailName && params.state?.ruleName) { + breadcrumb = [ + ...breadcrumb, + { + text: i18nRules.EDIT_PAGE_TITLE, + href: '', + }, + ]; + } + + if (!isRuleEditPage(params.pathName) && params.state && !params.state.isExistingRule) { + breadcrumb = [...breadcrumb, { text: DELETED_RULE, href: '' }]; + } + + return breadcrumb; +}; 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 3f8f6315f3471..805ac2d37741a 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 @@ -5,27 +5,13 @@ * 2.0. */ -import type { ChromeBreadcrumb } from '@kbn/core/public'; import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; import { isThreatMatchRule } from '../../../../../common/detection_engine/utils'; import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations'; -import { - getRuleDetailsTabUrl, - getRuleDetailsUrl, -} from '../../../../common/components/link_to/redirect_to_detection_engine'; -import * as i18nRules from './translations'; -import type { RouteSpyState } from '../../../../common/utils/route/types'; -import { SecurityPageName } from '../../../../app/types'; -import { DEFAULT_THREAT_MATCH_QUERY, RULES_PATH } from '../../../../../common/constants'; +import { DEFAULT_THREAT_MATCH_QUERY } from '../../../../../common/constants'; import type { AboutStepRule, DefineStepRule, RuleStepsOrder, ScheduleStepRule } from './types'; import { DataSourceType, GroupByOptions, RuleStep } from './types'; -import type { GetSecuritySolutionUrl } from '../../../../common/components/link_to'; import { DEFAULT_SUPPRESSION_MISSING_FIELDS_STRATEGY } from '../../../../../common/detection_engine/rule_schema'; -import { - RuleDetailTabs, - RULE_DETAILS_TAB_NAME, -} from '../../../../detection_engine/rule_details_ui/pages/rule_details'; -import { DELETED_RULE } from '../../../../detection_engine/rule_details_ui/pages/rule_details/translations'; import { fillEmptySeverityMappings } from './helpers'; export const ruleStepsOrder: RuleStepsOrder = [ @@ -35,75 +21,6 @@ export const ruleStepsOrder: RuleStepsOrder = [ RuleStep.ruleActions, ]; -const getRuleDetailsTabName = (tabName: string): string => { - return RULE_DETAILS_TAB_NAME[tabName] ?? RULE_DETAILS_TAB_NAME[RuleDetailTabs.alerts]; -}; - -const isRuleCreatePage = (pathname: string) => - pathname.includes(RULES_PATH) && pathname.includes('/create'); - -const isRuleEditPage = (pathname: string) => - pathname.includes(RULES_PATH) && pathname.includes('/edit'); - -export const getTrailingBreadcrumbs = ( - params: RouteSpyState, - getSecuritySolutionUrl: GetSecuritySolutionUrl -): ChromeBreadcrumb[] => { - let breadcrumb: ChromeBreadcrumb[] = []; - - if (params.detailName && params.state?.ruleName) { - breadcrumb = [ - ...breadcrumb, - { - text: params.state.ruleName, - href: getSecuritySolutionUrl({ - deepLinkId: SecurityPageName.rules, - path: getRuleDetailsUrl(params.detailName, ''), - }), - }, - ]; - } - - if (params.detailName && params.state?.ruleName && params.tabName) { - breadcrumb = [ - ...breadcrumb, - { - text: getRuleDetailsTabName(params.tabName), - href: getSecuritySolutionUrl({ - deepLinkId: SecurityPageName.rules, - path: getRuleDetailsTabUrl(params.detailName, params.tabName, ''), - }), - }, - ]; - } - - if (isRuleCreatePage(params.pathName)) { - breadcrumb = [ - ...breadcrumb, - { - text: i18nRules.ADD_PAGE_TITLE, - href: '', - }, - ]; - } - - if (isRuleEditPage(params.pathName) && params.detailName && params.state?.ruleName) { - breadcrumb = [ - ...breadcrumb, - { - text: i18nRules.EDIT_PAGE_TITLE, - href: '', - }, - ]; - } - - if (!isRuleEditPage(params.pathName) && params.state && !params.state.isExistingRule) { - breadcrumb = [...breadcrumb, { text: DELETED_RULE, href: '' }]; - } - - return breadcrumb; -}; - export const threatDefault = [ { framework: 'MITRE ATT&CK', diff --git a/x-pack/plugins/security_solution/public/exceptions/utils/pages.utils.ts b/x-pack/plugins/security_solution/public/exceptions/utils/breadcrumbs.ts similarity index 61% rename from x-pack/plugins/security_solution/public/exceptions/utils/pages.utils.ts rename to x-pack/plugins/security_solution/public/exceptions/utils/breadcrumbs.ts index 9c1a3289aca6f..a4f37ef18feec 100644 --- a/x-pack/plugins/security_solution/public/exceptions/utils/pages.utils.ts +++ b/x-pack/plugins/security_solution/public/exceptions/utils/breadcrumbs.ts @@ -6,16 +6,17 @@ */ import type { ChromeBreadcrumb } from '@kbn/core/public'; import { EXCEPTIONS_PATH } from '../../../common/constants'; -import type { GetSecuritySolutionUrl } from '../../common/components/link_to'; -import type { RouteSpyState } from '../../common/utils/route/types'; +import type { GetTrailingBreadcrumbs } from '../../common/components/navigation/breadcrumbs/types'; const isListDetailPage = (pathname: string) => pathname.includes(EXCEPTIONS_PATH) && pathname.includes('/details'); -export const getTrailingBreadcrumbs = ( - params: RouteSpyState, - getSecuritySolutionUrl: GetSecuritySolutionUrl -): ChromeBreadcrumb[] => { +/** + * This module should only export this function. + * All the `getTrailingBreadcrumbs` functions in Security are loaded into the main bundle. + * We should be careful to not import unnecessary modules in this file to avoid increasing the main app bundle size. + */ +export const getTrailingBreadcrumbs: GetTrailingBreadcrumbs = (params, getSecuritySolutionUrl) => { let breadcrumb: ChromeBreadcrumb[] = []; if (isListDetailPage(params.pathName) && params.state?.listName) { diff --git a/x-pack/plugins/security_solution/public/explore/hosts/pages/details/utils.ts b/x-pack/plugins/security_solution/public/explore/hosts/pages/details/breadcrumbs.ts similarity index 78% rename from x-pack/plugins/security_solution/public/explore/hosts/pages/details/utils.ts rename to x-pack/plugins/security_solution/public/explore/hosts/pages/details/breadcrumbs.ts index 634f4e3889cb6..4b41229c4a1a0 100644 --- a/x-pack/plugins/security_solution/public/explore/hosts/pages/details/utils.ts +++ b/x-pack/plugins/security_solution/public/explore/hosts/pages/details/breadcrumbs.ts @@ -8,16 +8,13 @@ import { get } from 'lodash/fp'; import type { ChromeBreadcrumb } from '@kbn/core/public'; -import { hostsModel } from '../../store'; import { HostsTableType } from '../../store/model'; import { getHostDetailsUrl } from '../../../../common/components/link_to/redirect_to_hosts'; import * as i18n from '../translations'; import type { HostRouteSpyState } from '../../../../common/utils/route/types'; import { SecurityPageName } from '../../../../app/types'; -import type { GetSecuritySolutionUrl } from '../../../../common/components/link_to'; - -export const type = hostsModel.HostsType.details; +import type { GetTrailingBreadcrumbs } from '../../../../common/components/navigation/breadcrumbs/types'; const TabNameMappedToI18nKey: Record = { [HostsTableType.hosts]: i18n.NAVIGATION_ALL_HOSTS_TITLE, @@ -29,10 +26,15 @@ const TabNameMappedToI18nKey: Record = { [HostsTableType.sessions]: i18n.NAVIGATION_SESSIONS_TITLE, }; -export const getTrailingBreadcrumbs = ( - params: HostRouteSpyState, - getSecuritySolutionUrl: GetSecuritySolutionUrl -): ChromeBreadcrumb[] => { +/** + * This module should only export this function. + * All the `getTrailingBreadcrumbs` functions in Security are loaded into the main bundle. + * We should be careful to not import unnecessary modules in this file to avoid increasing the main app bundle size. + */ +export const getTrailingBreadcrumbs: GetTrailingBreadcrumbs = ( + params, + getSecuritySolutionUrl +) => { let breadcrumb: ChromeBreadcrumb[] = []; if (params.detailName != null) { diff --git a/x-pack/plugins/security_solution/public/explore/hosts/pages/details/details_tabs.test.tsx b/x-pack/plugins/security_solution/public/explore/hosts/pages/details/details_tabs.test.tsx index 611c1fb95a995..df38f06888539 100644 --- a/x-pack/plugins/security_solution/public/explore/hosts/pages/details/details_tabs.test.tsx +++ b/x-pack/plugins/security_solution/public/explore/hosts/pages/details/details_tabs.test.tsx @@ -20,10 +20,9 @@ import { } from '../../../../common/mock'; import { HostDetailsTabs } from './details_tabs'; import { hostDetailsPagePath } from '../types'; -import { type } from './utils'; import { useMountAppended } from '../../../../common/utils/use_mount_appended'; import { getHostDetailsPageFilters } from './helpers'; -import { HostsTableType } from '../../store/model'; +import { HostsType, HostsTableType } from '../../store/model'; import { mockCasesContract } from '@kbn/cases-plugin/public/mocks'; import type { State } from '../../../../common/store'; import { createStore } from '../../../../common/store'; @@ -123,7 +122,7 @@ describe('body', () => { hostDetailsPagePath={hostDetailsPagePath} indexNames={[]} indexPattern={mockIndexPattern} - type={type} + type={HostsType.details} hostDetailsFilter={mockHostDetailsPageFilters} filterQuery={filterQuery} from={'2020-07-07T08:20:18.966Z'} diff --git a/x-pack/plugins/security_solution/public/explore/hosts/pages/details/details_tabs.tsx b/x-pack/plugins/security_solution/public/explore/hosts/pages/details/details_tabs.tsx index 13955a6e6b013..cc21c96ac9405 100644 --- a/x-pack/plugins/security_solution/public/explore/hosts/pages/details/details_tabs.tsx +++ b/x-pack/plugins/security_solution/public/explore/hosts/pages/details/details_tabs.tsx @@ -10,14 +10,13 @@ import { Routes, Route } from '@kbn/shared-ux-router'; import { TableId } from '@kbn/securitysolution-data-table'; import { RiskScoreEntity } from '../../../../../common/search_strategy'; import { RiskDetailsTabBody } from '../../../components/risk_score/risk_details_tab_body'; -import { HostsTableType } from '../../store/model'; +import { HostsType, HostsTableType } from '../../store/model'; import { AnomaliesQueryTabBody } from '../../../../common/containers/anomalies/anomalies_query_tab_body'; import { useGlobalTime } from '../../../../common/containers/use_global_time'; import { AnomaliesHostTable } from '../../../../common/components/ml/tables/anomalies_host_table'; import { EventsQueryTabBody } from '../../../../common/components/events_tab'; import type { HostDetailsTabsProps } from './types'; -import { type } from './utils'; import { AuthenticationsQueryTabBody, @@ -43,7 +42,7 @@ export const HostDetailsTabs = React.memo( skip: isInitializing || filterQuery === undefined, setQuery, startDate: from, - type, + type: HostsType.details, indexPattern, indexNames, hostName: detailName, diff --git a/x-pack/plugins/security_solution/public/explore/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/explore/hosts/pages/details/index.tsx index 107cdb8af4de6..5d0ff73569bdc 100644 --- a/x-pack/plugins/security_solution/public/explore/hosts/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/explore/hosts/pages/details/index.tsx @@ -49,7 +49,7 @@ import { SpyRoute } from '../../../../common/utils/route/spy_routes'; import { HostDetailsTabs } from './details_tabs'; import { navTabsHostDetails } from './nav_tabs'; import type { HostDetailsProps } from './types'; -import { type } from './utils'; +import { HostsType } from '../../store/model'; import { getHostDetailsPageFilters } from './helpers'; import { showGlobalFilters } from '../../../../timelines/components/timeline/helpers'; import { useGlobalFullScreen } from '../../../../common/containers/use_full_screen'; @@ -269,7 +269,7 @@ const HostDetailsComponent: React.FC = ({ detailName, hostDeta to={to} from={from} detailName={detailName} - type={type} + type={HostsType.details} setQuery={setQuery} filterQuery={stringifiedAdditionalFilters} hostDetailsPagePath={hostDetailsPagePath} diff --git a/x-pack/plugins/security_solution/public/explore/network/pages/details/utils.ts b/x-pack/plugins/security_solution/public/explore/network/pages/details/breadcrumbs.ts similarity index 79% rename from x-pack/plugins/security_solution/public/explore/network/pages/details/utils.ts rename to x-pack/plugins/security_solution/public/explore/network/pages/details/breadcrumbs.ts index ba8bb5ec7acd4..d3aaa9fba6af7 100644 --- a/x-pack/plugins/security_solution/public/explore/network/pages/details/utils.ts +++ b/x-pack/plugins/security_solution/public/explore/network/pages/details/breadcrumbs.ts @@ -10,15 +10,13 @@ import { get } from 'lodash/fp'; import type { ChromeBreadcrumb } from '@kbn/core/public'; import { decodeIpv6 } from '../../../../common/lib/helpers'; import { getNetworkDetailsUrl } from '../../../../common/components/link_to/redirect_to_network'; -import { networkModel } from '../../store'; import * as i18n from '../translations'; import { NetworkDetailsRouteType } from './types'; import type { NetworkRouteSpyState } from '../../../../common/utils/route/types'; import { SecurityPageName } from '../../../../app/types'; -import type { GetSecuritySolutionUrl } from '../../../../common/components/link_to'; import { NetworkRouteType } from '../navigation/types'; +import type { GetTrailingBreadcrumbs } from '../../../../common/components/navigation/breadcrumbs/types'; -export const type = networkModel.NetworkType.details; const TabNameMappedToI18nKey: Record = { [NetworkDetailsRouteType.events]: i18n.NAVIGATION_EVENTS_TITLE, [NetworkDetailsRouteType.anomalies]: i18n.NAVIGATION_ANOMALIES_TITLE, @@ -28,11 +26,15 @@ const TabNameMappedToI18nKey: Record { +/** + * This module should only export this function. + * All the `getTrailingBreadcrumbs` functions in Security are loaded into the main bundle. + * We should be careful to not import unnecessary modules in this file to avoid increasing the main app bundle size. + */ +export const getTrailingBreadcrumbs: GetTrailingBreadcrumbs = ( + params, + getSecuritySolutionUrl +) => { let breadcrumb: ChromeBreadcrumb[] = []; if (params.detailName != null) { diff --git a/x-pack/plugins/security_solution/public/explore/network/pages/details/index.tsx b/x-pack/plugins/security_solution/public/explore/network/pages/details/index.tsx index b3582262a691a..70f3dc56887fe 100644 --- a/x-pack/plugins/security_solution/public/explore/network/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/explore/network/pages/details/index.tsx @@ -58,8 +58,6 @@ import { SecurityCellActionsTrigger, } from '../../../../common/components/cell_actions'; -export { getTrailingBreadcrumbs } from './utils'; - const NetworkDetailsManage = manageQuery(IpOverview); const NetworkDetailsComponent: React.FC = () => { diff --git a/x-pack/plugins/security_solution/public/explore/users/pages/details/utils.ts b/x-pack/plugins/security_solution/public/explore/users/pages/details/breadcrumbs.ts similarity index 77% rename from x-pack/plugins/security_solution/public/explore/users/pages/details/utils.ts rename to x-pack/plugins/security_solution/public/explore/users/pages/details/breadcrumbs.ts index c4ffe7c84e2a8..d2f793417fe32 100644 --- a/x-pack/plugins/security_solution/public/explore/users/pages/details/utils.ts +++ b/x-pack/plugins/security_solution/public/explore/users/pages/details/breadcrumbs.ts @@ -8,16 +8,13 @@ import { get } from 'lodash/fp'; import type { ChromeBreadcrumb } from '@kbn/core/public'; -import { usersModel } from '../../store'; import { UsersTableType } from '../../store/model'; import { getUsersDetailsUrl } from '../../../../common/components/link_to/redirect_to_users'; import * as i18n from '../translations'; import type { UsersRouteSpyState } from '../../../../common/utils/route/types'; import { SecurityPageName } from '../../../../app/types'; -import type { GetSecuritySolutionUrl } from '../../../../common/components/link_to'; - -export const type = usersModel.UsersType.details; +import type { GetTrailingBreadcrumbs } from '../../../../common/components/navigation/breadcrumbs/types'; const TabNameMappedToI18nKey: Record = { [UsersTableType.allUsers]: i18n.NAVIGATION_ALL_USERS_TITLE, @@ -28,10 +25,15 @@ const TabNameMappedToI18nKey: Record = { [UsersTableType.risk]: i18n.NAVIGATION_RISK_TITLE, }; -export const getTrailingBreadcrumbs = ( - params: UsersRouteSpyState, - getSecuritySolutionUrl: GetSecuritySolutionUrl -): ChromeBreadcrumb[] => { +/** + * This module should only export this function. + * All the `getTrailingBreadcrumbs` functions in Security are loaded into the main bundle. + * We should be careful to not import unnecessary modules in this file to avoid increasing the main app bundle size. + */ +export const getTrailingBreadcrumbs: GetTrailingBreadcrumbs = ( + params, + getSecuritySolutionUrl +) => { let breadcrumb: ChromeBreadcrumb[] = []; if (params.detailName != null) { diff --git a/x-pack/plugins/security_solution/public/explore/users/pages/details/index.tsx b/x-pack/plugins/security_solution/public/explore/users/pages/details/index.tsx index 0046d9aa6f61f..3612fd784d518 100644 --- a/x-pack/plugins/security_solution/public/explore/users/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/explore/users/pages/details/index.tsx @@ -41,7 +41,6 @@ import { SpyRoute } from '../../../../common/utils/route/spy_routes'; import { UsersDetailsTabs } from './details_tabs'; import { navTabsUsersDetails } from './nav_tabs'; import type { UsersDetailsProps } from './types'; -import { type } from './utils'; import { getUsersDetailsPageFilters } from './helpers'; import { showGlobalFilters } from '../../../../timelines/components/timeline/helpers'; import { useGlobalFullScreen } from '../../../../common/containers/use_full_screen'; @@ -257,7 +256,7 @@ const UsersDetailsComponent: React.FC = ({ userDetailFilter={usersDetailsPageFilters} setQuery={setQuery} to={to} - type={type} + type={UsersType.details} usersDetailsPagePath={usersDetailsPagePath} /> diff --git a/x-pack/plugins/security_solution/public/index.ts b/x-pack/plugins/security_solution/public/index.ts index 87f2bcae2e5f3..8d0203e32fb0f 100644 --- a/x-pack/plugins/security_solution/public/index.ts +++ b/x-pack/plugins/security_solution/public/index.ts @@ -9,6 +9,8 @@ import type { PluginInitializerContext } from '@kbn/core/public'; import { Plugin } from './plugin'; import type { PluginSetup, PluginStart } from './types'; export type { TimelineModel } from './timelines/store/timeline/model'; +export type { NavigationLink } from './common/links'; + export type { UpsellingService, PageUpsellings, diff --git a/x-pack/plugins/security_solution/public/kubernetes/pages/utils/breadcrumbs.ts b/x-pack/plugins/security_solution/public/kubernetes/pages/utils/breadcrumbs.ts index 111a0dc3be554..9dcd76b838cac 100644 --- a/x-pack/plugins/security_solution/public/kubernetes/pages/utils/breadcrumbs.ts +++ b/x-pack/plugins/security_solution/public/kubernetes/pages/utils/breadcrumbs.ts @@ -6,15 +6,16 @@ */ import type { ChromeBreadcrumb } from '@kbn/core/public'; -import type { RouteSpyState } from '../../../common/utils/route/types'; import { SecurityPageName } from '../../../app/types'; -import type { GetSecuritySolutionUrl } from '../../../common/components/link_to'; import { getKubernetesDetailsUrl } from '../../../common/components/link_to'; +import type { GetTrailingBreadcrumbs } from '../../../common/components/navigation/breadcrumbs/types'; -export const getTrailingBreadcrumbs = ( - params: RouteSpyState, - getSecuritySolutionUrl: GetSecuritySolutionUrl -): ChromeBreadcrumb[] => { +/** + * This module should only export this function. + * All the `getTrailingBreadcrumbs` functions in Security are loaded into the main bundle. + * We should be careful to not import unnecessary modules in this file to avoid increasing the main app bundle size. + */ +export const getTrailingBreadcrumbs: GetTrailingBreadcrumbs = (params, getSecuritySolutionUrl) => { let breadcrumb: ChromeBreadcrumb[] = []; if (params.detailName != null) { diff --git a/x-pack/plugins/security_solution/public/mocks.ts b/x-pack/plugins/security_solution/public/mocks.ts index f16e81636846c..0a1072f5fd22f 100644 --- a/x-pack/plugins/security_solution/public/mocks.ts +++ b/x-pack/plugins/security_solution/public/mocks.ts @@ -6,6 +6,7 @@ */ import { BehaviorSubject } from 'rxjs'; +import type { BreadcrumbsNav } from './common/breadcrumbs'; import type { NavigationLink } from './common/links/types'; const setupMock = () => ({ @@ -15,6 +16,10 @@ const setupMock = () => ({ const startMock = () => ({ getNavLinks$: jest.fn(() => new BehaviorSubject([])), setIsSidebarEnabled: jest.fn(), + setGetStartedPage: jest.fn(), + getBreadcrumbsNav$: jest.fn( + () => new BehaviorSubject({ leading: [], trailing: [] }) + ), }); export const securitySolutionMock = { diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 929d38a445cd8..70c1cb7667752 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import { BehaviorSubject, Subject } from 'rxjs'; +import { Subject } from 'rxjs'; import type * as H from 'history'; import type { AppMountParameters, @@ -36,7 +36,6 @@ import { APP_ID, APP_UI_ID, APP_PATH, APP_ICON_SOLUTION } from '../common/consta import { updateAppLinks, type LinksPermissions } from './common/links'; import { registerDeepLinksUpdater } from './common/links/deep_links'; -import { navLinks$ } from './common/links/nav_links'; import { licenseService } from './common/hooks/use_license'; import type { SecuritySolutionUiConfigType } from './common/types'; import { ExperimentalFeaturesService } from './common/experimental_features_service'; @@ -49,10 +48,10 @@ import { getLazyEndpointPolicyResponseExtension } from './management/pages/polic import { getLazyEndpointGenericErrorsListExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_generic_errors_list'; import type { ExperimentalFeatures } from '../common/experimental_features'; import { parseExperimentalConfigValue } from '../common/experimental_features'; -import { UpsellingService } from './common/lib/upsellings'; import { LazyEndpointCustomAssetsExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_custom_assets_extension'; import type { SecurityAppStore } from './common/store/types'; +import { PluginContract } from './plugin_contract'; export class Plugin implements IPlugin { /** @@ -76,12 +75,10 @@ export class Plugin implements IPlugin; - private getStartedComponent$: BehaviorSubject; constructor(private readonly initializerContext: PluginInitializerContext) { this.config = this.initializerContext.config.get(); @@ -91,9 +88,7 @@ export class Plugin implements IPlugin(true); - this.getStartedComponent$ = new BehaviorSubject(null); - this.upsellingService = new UpsellingService(); + this.contract = new PluginContract(); this.telemetry = new TelemetryService(); } private appUpdater$ = new Subject(); @@ -158,6 +153,7 @@ export class Plugin implements IPlugin SecuritySolutionTemplateWrapper, }, savedObjectsManagement: startPluginsDeps.savedObjectsManagement, - isSidebarEnabled$: this.isSidebarEnabled$, - getStartedComponent$: this.getStartedComponent$, - upselling: this.upsellingService, telemetry: this.telemetry.start(), }; return services; @@ -235,19 +228,7 @@ export class Plugin implements IPlugin { - /** - * The specially formatted comment in the `import` expression causes the corresponding webpack chunk to be named. This aids us in debugging chunk size issues. - * See https://webpack.js.org/api/module-methods/#magic-comments - */ - const { resolverPluginSetup } = await import( - /* webpackChunkName: "resolver" */ './resolver' - ); - return resolverPluginSetup(); - }, - upselling: this.upsellingService, - }; + return this.contract.getSetupContract(); } public start(core: CoreStart, plugins: StartPlugins): PluginStart { @@ -310,19 +291,12 @@ export class Plugin implements IPlugin navLinks$, - setIsSidebarEnabled: (isSidebarEnabled: boolean) => - this.isSidebarEnabled$.next(isSidebarEnabled), - setGetStartedPage: (getStartedComponent) => { - this.getStartedComponent$.next(getStartedComponent); - }, - }; + return this.contract.getStartContract(); } public stop() { licenseService.stop(); - return {}; + return this.contract.getStopContract(); } private lazyHelpersForRoutes() { @@ -492,13 +466,14 @@ export class Plugin implements IPlugin { const linksPermissions: LinksPermissions = { experimentalFeatures: this.experimentalFeatures, - upselling: this.upsellingService, + upselling, capabilities: core.application.capabilities, }; diff --git a/x-pack/plugins/security_solution/public/plugin_contract.ts b/x-pack/plugins/security_solution/public/plugin_contract.ts new file mode 100644 index 0000000000000..583424509e131 --- /dev/null +++ b/x-pack/plugins/security_solution/public/plugin_contract.ts @@ -0,0 +1,67 @@ +/* + * 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 { BehaviorSubject } from 'rxjs'; +import { UpsellingService } from './common/lib/upsellings'; +import type { ContractStartServices, PluginSetup, PluginStart } from './types'; +import { navLinks$ } from './common/links/nav_links'; +import { breadcrumbsNav$ } from './common/breadcrumbs'; + +export class PluginContract { + public isSidebarEnabled$: BehaviorSubject; + public getStartedComponent$: BehaviorSubject; + public upsellingService: UpsellingService; + + constructor() { + this.isSidebarEnabled$ = new BehaviorSubject(true); + this.getStartedComponent$ = new BehaviorSubject(null); + this.upsellingService = new UpsellingService(); + } + + public getStartServices(): ContractStartServices { + return { + isSidebarEnabled$: this.isSidebarEnabled$.asObservable(), + getStartedComponent$: this.getStartedComponent$.asObservable(), + upselling: this.upsellingService, + }; + } + + public getSetupContract(): PluginSetup { + return { + resolver: lazyResolver, + upselling: this.upsellingService, + }; + } + + public getStartContract(): PluginStart { + return { + getNavLinks$: () => navLinks$, + setIsSidebarEnabled: (isSidebarEnabled: boolean) => + this.isSidebarEnabled$.next(isSidebarEnabled), + setGetStartedPage: (getStartedComponent) => { + this.getStartedComponent$.next(getStartedComponent); + }, + getBreadcrumbsNav$: () => breadcrumbsNav$, + }; + } + + public getStopContract() { + return {}; + } +} + +const lazyResolver = async () => { + /** + * The specially formatted comment in the `import` expression causes the corresponding webpack chunk to be named. This aids us in debugging chunk size issues. + * See https://webpack.js.org/api/module-methods/#magic-comments + */ + const { resolverPluginSetup } = await import( + /* webpackChunkName: "resolver" */ + './resolver' + ); + return resolverPluginSetup(); +}; diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index 2687c1fcab25e..31765d2395176 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { BehaviorSubject, Observable } from 'rxjs'; +import type { Observable } from 'rxjs'; import type { AppLeaveHandler, CoreStart } from '@kbn/core/public'; import type { HomePublicPluginSetup } from '@kbn/home-plugin/public'; @@ -70,6 +70,7 @@ import type { NavigationLink } from './common/links'; import type { TelemetryClientStart } from './common/lib/telemetry'; import type { Dashboards } from './dashboards'; import type { UpsellingService } from './common/lib/upsellings'; +import type { BreadcrumbsNav } from './common/breadcrumbs/types'; export interface SetupPlugins { cloud?: CloudSetup; @@ -127,8 +128,15 @@ export interface StartPluginsDependencies extends StartPlugins { savedObjectsTaggingOss: SavedObjectTaggingOssPluginStart; } +export interface ContractStartServices { + isSidebarEnabled$: Observable; + getStartedComponent$: Observable; + upselling: UpsellingService; +} + export type StartServices = CoreStart & - StartPlugins & { + StartPlugins & + ContractStartServices & { storage: Storage; sessionStorage: Storage; apm: ApmBase; @@ -143,9 +151,6 @@ export type StartServices = CoreStart & getPluginWrapper: () => typeof SecuritySolutionTemplateWrapper; }; savedObjectsManagement: SavedObjectsManagementPluginStart; - isSidebarEnabled$: BehaviorSubject; - getStartedComponent$: BehaviorSubject; - upselling: UpsellingService; telemetry: TelemetryClientStart; }; @@ -158,6 +163,7 @@ export interface PluginStart { getNavLinks$: () => Observable; setIsSidebarEnabled: (isSidebarEnabled: boolean) => void; setGetStartedPage: (getStartedComponent: React.ComponentType) => void; + getBreadcrumbsNav$: () => Observable; } export interface AppObservableLibs { diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json index bb7c6ce5ea4b6..d272cb53f4d80 100644 --- a/x-pack/plugins/security_solution/tsconfig.json +++ b/x-pack/plugins/security_solution/tsconfig.json @@ -83,7 +83,6 @@ "@kbn/guided-onboarding-plugin", "@kbn/i18n-react", "@kbn/kibana-react-plugin", - "@kbn/core-chrome-browser", "@kbn/ecs-data-quality-dashboard", "@kbn/elastic-assistant", "@kbn/data-views-plugin", diff --git a/x-pack/plugins/serverless_security/public/common/jest.config.js b/x-pack/plugins/serverless_security/public/common/jest.config.js new file mode 100644 index 0000000000000..3ea98b5178343 --- /dev/null +++ b/x-pack/plugins/serverless_security/public/common/jest.config.js @@ -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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../..', + roots: ['/x-pack/plugins/serverless_security/public/common'], + testMatch: [ + '/x-pack/plugins/serverless_security/public/common/**/*.test.{js,mjs,ts,tsx}', + ], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/serverless_security/public/common', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/serverless_security/public/common/**/*.{ts,tsx}', + '!/x-pack/plugins/serverless_security/public/common/*.test.{ts,tsx}', + '!/x-pack/plugins/serverless_security/public/common/{__test__,__snapshots__,__examples__,*mock*,tests,test_helpers,integration_tests,types}/**/*', + '!/x-pack/plugins/serverless_security/public/common/*mock*.{ts,tsx}', + '!/x-pack/plugins/serverless_security/public/common/*.test.{ts,tsx}', + '!/x-pack/plugins/serverless_security/public/common/*.d.ts', + '!/x-pack/plugins/serverless_security/public/common/*.config.ts', + '!/x-pack/plugins/serverless_security/public/common/index.{js,ts,tsx}', + ], +}; diff --git a/x-pack/plugins/serverless_security/public/common/navigation/breadcrumbs.ts b/x-pack/plugins/serverless_security/public/common/navigation/breadcrumbs.ts new file mode 100644 index 0000000000000..d4e9d13b37698 --- /dev/null +++ b/x-pack/plugins/serverless_security/public/common/navigation/breadcrumbs.ts @@ -0,0 +1,15 @@ +/* + * 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 { Services } from '../services'; + +export const subscribeBreadcrumbs = (services: Services) => { + const { securitySolution, serverless } = services; + securitySolution.getBreadcrumbsNav$().subscribe((breadcrumbsNav) => { + serverless.setBreadcrumbs(breadcrumbsNav.trailing); + }); +}; diff --git a/x-pack/plugins/serverless_security/public/common/navigation/links/index.ts b/x-pack/plugins/serverless_security/public/common/navigation/links/index.ts new file mode 100644 index 0000000000000..7271c36bbdfdf --- /dev/null +++ b/x-pack/plugins/serverless_security/public/common/navigation/links/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +export { getProjectNavLinks$ } from './nav_links'; +export type { ProjectNavLinks, ProjectNavigationLink } from './types'; diff --git a/x-pack/plugins/serverless_security/public/common/navigation/links/nav_links.ts b/x-pack/plugins/serverless_security/public/common/navigation/links/nav_links.ts new file mode 100644 index 0000000000000..289d0a0c55708 --- /dev/null +++ b/x-pack/plugins/serverless_security/public/common/navigation/links/nav_links.ts @@ -0,0 +1,18 @@ +/* + * 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 { map, type Observable } from 'rxjs'; +import type { NavigationLink } from '@kbn/security-solution-plugin/public'; +import type { ProjectNavLinks, ProjectNavigationLink } from './types'; + +export const getProjectNavLinks$ = (navLinks$: Observable): ProjectNavLinks => { + return navLinks$.pipe(map(processNavLinks)); +}; + +// TODO: This is a placeholder function that will be used to process the nav links, +// It will mix internal Security nav links with the external links to other plugins, in the correct order. +const processNavLinks = (navLinks: NavigationLink[]): ProjectNavigationLink[] => navLinks; diff --git a/x-pack/plugins/serverless_security/public/common/navigation/links/types.ts b/x-pack/plugins/serverless_security/public/common/navigation/links/types.ts new file mode 100644 index 0000000000000..47930f64dd6d8 --- /dev/null +++ b/x-pack/plugins/serverless_security/public/common/navigation/links/types.ts @@ -0,0 +1,16 @@ +/* + * 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 type { Observable } from 'rxjs'; +import type { NavigationLink } from '@kbn/security-solution-plugin/public'; + +export interface ProjectNavigationLink extends NavigationLink { + // The appId for external links + appId?: string; +} + +export type ProjectNavLinks = Observable; diff --git a/x-pack/plugins/serverless_security/public/common/navigation/navigation_tree.test.ts b/x-pack/plugins/serverless_security/public/common/navigation/navigation_tree.test.ts new file mode 100644 index 0000000000000..91d020c332702 --- /dev/null +++ b/x-pack/plugins/serverless_security/public/common/navigation/navigation_tree.test.ts @@ -0,0 +1,286 @@ +/* + * 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 type { ChromeNavLink } from '@kbn/core/public'; +import { APP_UI_ID, SecurityPageName } from '@kbn/security-solution-plugin/common'; +import { servicesMocks } from '../services.mock'; +import { subscribeNavigationTree } from './navigation_tree'; +import { BehaviorSubject } from 'rxjs'; +import { mockProjectNavLinks } from '../services.mock'; +import type { ProjectNavigationLink } from './links'; + +const mockChromeNavLinks = jest.fn((): ChromeNavLink[] => []); +const mockChromeGetNavLinks = jest.fn(() => new BehaviorSubject(mockChromeNavLinks())); +const mockChromeNavLinksGet = jest.fn((id: string): ChromeNavLink | undefined => + mockChromeNavLinks().find((link) => link.id === id) +); +const mockChromeNavLinksHas = jest.fn((id: string): boolean => + mockChromeNavLinks().some((link) => link.id === id) +); + +const mockServices = { + ...servicesMocks, + chrome: { + ...servicesMocks.chrome, + navLinks: { + ...servicesMocks.chrome.navLinks, + get: mockChromeNavLinksGet, + has: mockChromeNavLinksHas, + getNavLinks$: mockChromeGetNavLinks, + }, + }, +}; + +const link1Id = 'link-1' as SecurityPageName; +const link2Id = 'link-2' as SecurityPageName; + +const link1: ProjectNavigationLink = { id: link1Id, title: 'link 1' }; +const link2: ProjectNavigationLink = { id: link2Id, title: 'link 2' }; + +const chromeNavLink1: ChromeNavLink = { + id: `${APP_UI_ID}:${link1.id}`, + title: link1.title, + href: '/link1', + url: '/link1', + baseUrl: '', +}; +const chromeNavLink2: ChromeNavLink = { + id: `${APP_UI_ID}:${link2.id}`, + title: link2.title, + href: '/link2', + url: '/link2', + baseUrl: '', +}; + +const waitForDebounce = async () => new Promise((resolve) => setTimeout(resolve, 150)); + +describe('subscribeNavigationTree', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockChromeNavLinks.mockReturnValue([chromeNavLink1, chromeNavLink2]); + }); + + it('should call serverless setNavigation', async () => { + mockProjectNavLinks.mockReturnValueOnce([link1]); + + subscribeNavigationTree(mockServices); + await waitForDebounce(); + + expect(mockServices.serverless.setNavigation).toHaveBeenCalledWith({ + navigationTree: [ + { + id: 'root', + title: 'Root', + path: ['root'], + breadcrumbStatus: 'hidden', + children: [ + { + id: chromeNavLink1.id, + title: link1.title, + path: ['root', chromeNavLink1.id], + deepLink: chromeNavLink1, + }, + ], + }, + ], + }); + }); + + it('should call serverless setNavigation with external link', async () => { + const externalLink = { ...link1, appId: 'externalAppId' }; + const chromeNavLinkExpected = { + ...chromeNavLink1, + id: `${externalLink.appId}:${externalLink.id}`, + }; + mockChromeNavLinks.mockReturnValue([chromeNavLinkExpected]); + mockProjectNavLinks.mockReturnValueOnce([externalLink]); + + subscribeNavigationTree(mockServices); + await waitForDebounce(); + + expect(mockServices.serverless.setNavigation).toHaveBeenCalledWith({ + navigationTree: [ + { + id: 'root', + title: 'Root', + path: ['root'], + breadcrumbStatus: 'hidden', + children: [ + { + id: chromeNavLinkExpected.id, + title: externalLink.title, + path: ['root', chromeNavLinkExpected.id], + deepLink: chromeNavLinkExpected, + }, + ], + }, + ], + }); + }); + + it('should call serverless setNavigation with nested children', async () => { + mockProjectNavLinks.mockReturnValueOnce([{ ...link1, links: [link2] }]); + + subscribeNavigationTree(mockServices); + await waitForDebounce(); + + expect(mockServices.serverless.setNavigation).toHaveBeenCalledWith({ + navigationTree: [ + { + id: 'root', + title: 'Root', + path: ['root'], + breadcrumbStatus: 'hidden', + children: [ + { + id: chromeNavLink1.id, + title: link1.title, + path: ['root', chromeNavLink1.id], + deepLink: chromeNavLink1, + children: [ + { + id: chromeNavLink2.id, + title: link2.title, + path: ['root', chromeNavLink1.id, chromeNavLink2.id], + deepLink: chromeNavLink2, + }, + ], + }, + ], + }, + ], + }); + }); + + it('should not call serverless setNavigation when projectNavLinks is empty', async () => { + mockProjectNavLinks.mockReturnValueOnce([]); + + subscribeNavigationTree(mockServices); + await waitForDebounce(); + + expect(mockServices.serverless.setNavigation).not.toHaveBeenCalled(); + }); + + it('should not call serverless setNavigation when chrome navLinks is empty', async () => { + mockChromeNavLinks.mockReturnValue([]); + mockProjectNavLinks.mockReturnValueOnce([link1]); + + subscribeNavigationTree(mockServices); + await waitForDebounce(); + + expect(mockServices.serverless.setNavigation).not.toHaveBeenCalled(); + }); + + it('should debounce updates', async () => { + const id = 'expectedId' as SecurityPageName; + const linkExpected = { ...link1, id }; + const chromeNavLinkExpected = { ...chromeNavLink1, id: `${APP_UI_ID}:${id}` }; + + const chromeGetNavLinks$ = new BehaviorSubject([chromeNavLink1]); + mockChromeGetNavLinks.mockReturnValue(chromeGetNavLinks$); + + mockChromeNavLinks.mockReturnValue([chromeNavLink1, chromeNavLink2, chromeNavLinkExpected]); + mockProjectNavLinks.mockReturnValueOnce([linkExpected]); + + subscribeNavigationTree(mockServices); + + chromeGetNavLinks$.next([chromeNavLink1]); + chromeGetNavLinks$.next([chromeNavLink2]); + chromeGetNavLinks$.next([chromeNavLinkExpected]); + + expect(mockServices.serverless.setNavigation).not.toHaveBeenCalled(); + + await waitForDebounce(); + + expect(mockServices.serverless.setNavigation).toHaveBeenCalledTimes(1); + expect(mockServices.serverless.setNavigation).toHaveBeenCalledWith({ + navigationTree: [ + { + id: 'root', + title: 'Root', + path: ['root'], + breadcrumbStatus: 'hidden', + children: [ + { + id: chromeNavLinkExpected.id, + title: link1.title, + path: ['root', chromeNavLinkExpected.id], + deepLink: chromeNavLinkExpected, + }, + ], + }, + ], + }); + }); + + it('should not include links that are not in the chrome navLinks', async () => { + mockChromeNavLinks.mockReturnValue([chromeNavLink2]); + mockProjectNavLinks.mockReturnValueOnce([link1, link2]); + + subscribeNavigationTree(mockServices); + await waitForDebounce(); + + expect(mockServices.serverless.setNavigation).toHaveBeenCalledWith({ + navigationTree: [ + { + id: 'root', + title: 'Root', + path: ['root'], + breadcrumbStatus: 'hidden', + children: [ + { + id: chromeNavLink2.id, + title: link2.title, + path: ['root', chromeNavLink2.id], + deepLink: chromeNavLink2, + }, + ], + }, + ], + }); + }); + + it('should set hidden breadcrumb for blacklisted links', async () => { + const chromeNavLinkTest = { + ...chromeNavLink1, + id: `${APP_UI_ID}:${SecurityPageName.usersEvents}`, // userEvents link is blacklisted + }; + mockChromeNavLinks.mockReturnValue([chromeNavLinkTest, chromeNavLink2]); + mockProjectNavLinks.mockReturnValueOnce([ + { ...link1, id: SecurityPageName.usersEvents }, + link2, + ]); + + subscribeNavigationTree(mockServices); + await waitForDebounce(); + + expect(mockServices.serverless.setNavigation).toHaveBeenCalledWith({ + navigationTree: [ + { + id: 'root', + title: 'Root', + path: ['root'], + breadcrumbStatus: 'hidden', + children: [ + { + id: chromeNavLinkTest.id, + title: link1.title, + path: ['root', chromeNavLinkTest.id], + deepLink: chromeNavLinkTest, + breadcrumbStatus: 'hidden', + }, + { + id: chromeNavLink2.id, + title: link2.title, + path: ['root', chromeNavLink2.id], + deepLink: chromeNavLink2, + }, + ], + }, + ], + }); + }); +}); diff --git a/x-pack/plugins/serverless_security/public/common/navigation/navigation_tree.ts b/x-pack/plugins/serverless_security/public/common/navigation/navigation_tree.ts new file mode 100644 index 0000000000000..70464c0c53f5d --- /dev/null +++ b/x-pack/plugins/serverless_security/public/common/navigation/navigation_tree.ts @@ -0,0 +1,86 @@ +/* + * 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 type { ChromeNavLinks, ChromeProjectNavigationNode } from '@kbn/core-chrome-browser'; +import { APP_UI_ID, SecurityPageName } from '@kbn/security-solution-plugin/common'; +import { combineLatest, skipWhile, debounceTime } from 'rxjs'; +import type { Services } from '../services'; +import type { ProjectNavigationLink } from './links/types'; + +// We need to hide breadcrumbs for some pages (tabs) because they appear duplicated. +// These breadcrumbs are incorrectly processed as trailing breadcrumbs in SecuritySolution, because of `SpyRoute` architecture limitations. +// They are navLinks tree with a SecurityPageName, so they should be treated as leading breadcrumbs in ESS as well. +// TODO: Improve the breadcrumbs logic in `use_breadcrumbs_nav` to avoid this workaround. +const HIDDEN_BREADCRUMBS = new Set([ + SecurityPageName.networkDns, + SecurityPageName.networkHttp, + SecurityPageName.networkTls, + SecurityPageName.networkAnomalies, + SecurityPageName.networkEvents, + SecurityPageName.usersAuthentications, + SecurityPageName.usersAnomalies, + SecurityPageName.usersRisk, + SecurityPageName.usersEvents, + SecurityPageName.uncommonProcesses, + SecurityPageName.hostsAnomalies, + SecurityPageName.hostsEvents, + SecurityPageName.hostsRisk, + SecurityPageName.sessions, +]); + +export const subscribeNavigationTree = (services: Services): void => { + const { chrome, serverless, getProjectNavLinks$ } = services; + + combineLatest([ + getProjectNavLinks$().pipe(skipWhile((navLink) => navLink.length === 0)), + chrome.navLinks.getNavLinks$().pipe(skipWhile((chromeNavLinks) => chromeNavLinks.length === 0)), + ]) + .pipe(debounceTime(100)) // avoid multiple calls in a short time + .subscribe(([projectNavLinks]) => { + // The root link is temporary until the issue about having multiple links at first level is solved. + // TODO: Assign the navigationTree nodes when the issue is solved: + // const navigationTree = formatChromeProjectNavNodes(chrome.navLinks, projectNavLinks), + const navigationTree: ChromeProjectNavigationNode[] = [ + { + id: 'root', + title: 'Root', + path: ['root'], + breadcrumbStatus: 'hidden', + children: formatChromeProjectNavNodes(chrome.navLinks, projectNavLinks, ['root']), + }, + ]; + serverless.setNavigation({ navigationTree }); + }); +}; + +const formatChromeProjectNavNodes = ( + chromeNavLinks: ChromeNavLinks, + projectNavLinks: ProjectNavigationLink[], + path: string[] = [] +): ChromeProjectNavigationNode[] => + projectNavLinks.reduce((navNodes, navLink) => { + const { id: deepLinkId, appId = APP_UI_ID, links, title } = navLink; + + const id = deepLinkId ? `${appId}:${deepLinkId}` : appId; + + if (chromeNavLinks.has(id)) { + const breadcrumbHidden = appId === APP_UI_ID && HIDDEN_BREADCRUMBS.has(deepLinkId); + const link: ChromeProjectNavigationNode = { + id, + title, + path: [...path, id], + deepLink: chromeNavLinks.get(id), + ...(breadcrumbHidden && { breadcrumbStatus: 'hidden' }), + }; + + if (links?.length) { + link.children = formatChromeProjectNavNodes(chromeNavLinks, links, link.path); + } + navNodes.push(link); + } + return navNodes; + }, []); diff --git a/x-pack/plugins/serverless_security/public/services.mock.tsx b/x-pack/plugins/serverless_security/public/common/services.mock.tsx similarity index 74% rename from x-pack/plugins/serverless_security/public/services.mock.tsx rename to x-pack/plugins/serverless_security/public/common/services.mock.tsx index 142ebb2152e63..70f024842a340 100644 --- a/x-pack/plugins/serverless_security/public/services.mock.tsx +++ b/x-pack/plugins/serverless_security/public/common/services.mock.tsx @@ -11,12 +11,18 @@ import { coreMock } from '@kbn/core/public/mocks'; import { serverlessMock } from '@kbn/serverless/public/mocks'; import { securityMock } from '@kbn/security-plugin/public/mocks'; import { securitySolutionMock } from '@kbn/security-solution-plugin/public/mocks'; +import { BehaviorSubject } from 'rxjs'; +import type { ProjectNavigationLink } from './navigation/links'; +import type { Services } from './services'; -export const servicesMocks = { +export const mockProjectNavLinks = jest.fn((): ProjectNavigationLink[] => []); + +export const servicesMocks: Services = { ...coreMock.createStart(), serverless: serverlessMock.createStart(), security: securityMock.createStart(), securitySolution: securitySolutionMock.createStart(), + getProjectNavLinks$: jest.fn(() => new BehaviorSubject(mockProjectNavLinks())), }; export const KibanaServicesProvider = React.memo(({ children }) => ( diff --git a/x-pack/plugins/serverless_security/public/services.tsx b/x-pack/plugins/serverless_security/public/common/services.tsx similarity index 56% rename from x-pack/plugins/serverless_security/public/services.tsx rename to x-pack/plugins/serverless_security/public/common/services.tsx index b62b91d1bbfea..f3bfe1dbbfa1b 100644 --- a/x-pack/plugins/serverless_security/public/services.tsx +++ b/x-pack/plugins/serverless_security/public/common/services.tsx @@ -12,16 +12,27 @@ import { useKibana as useKibanaReact, } from '@kbn/kibana-react-plugin/public'; -import type { ServerlessSecurityPluginStartDependencies } from './types'; +import type { ServerlessSecurityPluginStartDependencies } from '../types'; +import { getProjectNavLinks$, type ProjectNavLinks } from './navigation/links'; -export type Services = CoreStart & ServerlessSecurityPluginStartDependencies; +interface InternalServices { + getProjectNavLinks$: () => ProjectNavLinks; +} +export type Services = CoreStart & ServerlessSecurityPluginStartDependencies & InternalServices; export const KibanaServicesProvider: React.FC<{ - core: CoreStart; - pluginsStart: ServerlessSecurityPluginStartDependencies; -}> = ({ core, pluginsStart, children }) => { - const services: Services = { ...core, ...pluginsStart }; + services: Services; +}> = ({ services, children }) => { return {children}; }; export const useKibana = () => useKibanaReact(); + +export const createServices = ( + core: CoreStart, + pluginsStart: ServerlessSecurityPluginStartDependencies +): Services => { + const { securitySolution } = pluginsStart; + const projectNavLinks$ = getProjectNavLinks$(securitySolution.getNavLinks$()); + return { ...core, ...pluginsStart, getProjectNavLinks$: () => projectNavLinks$ }; +}; diff --git a/x-pack/plugins/serverless_security/public/components/get_started/index.tsx b/x-pack/plugins/serverless_security/public/components/get_started/index.tsx index 48326496d4422..0ee8c7a1ce62d 100644 --- a/x-pack/plugins/serverless_security/public/components/get_started/index.tsx +++ b/x-pack/plugins/serverless_security/public/components/get_started/index.tsx @@ -7,21 +7,17 @@ import React from 'react'; -import { CoreStart } from '@kbn/core/public'; - +import { KibanaServicesProvider, type Services } from '../../common/services'; import type { GetStartedComponent } from './types'; import { GetStarted } from './lazy'; -import { KibanaServicesProvider } from '../../services'; -import { ServerlessSecurityPluginStartDependencies } from '../../types'; import { SecurityProductTypes } from '../../../common/config'; export const getSecurityGetStartedComponent = ( - core: CoreStart, - pluginsStart: ServerlessSecurityPluginStartDependencies, + services: Services, productTypes: SecurityProductTypes ): GetStartedComponent => { return () => ( - + ); diff --git a/x-pack/plugins/serverless_security/public/components/get_started/toggle_panel.test.tsx b/x-pack/plugins/serverless_security/public/components/get_started/toggle_panel.test.tsx index 53f51cdc09a8b..1a924b70fc45f 100644 --- a/x-pack/plugins/serverless_security/public/components/get_started/toggle_panel.test.tsx +++ b/x-pack/plugins/serverless_security/public/components/get_started/toggle_panel.test.tsx @@ -17,14 +17,6 @@ jest.mock('@elastic/eui', () => ({ useEuiShadow: jest.fn(), })); -jest.mock('../../services', () => ({ - useKibana: jest.fn(() => ({ - services: { - storage: {}, - }, - })), -})); - jest.mock('../../lib/get_started/storage'); jest.mock('./use_setup_cards', () => ({ diff --git a/x-pack/plugins/serverless_security/public/components/side_navigation/index.tsx b/x-pack/plugins/serverless_security/public/components/side_navigation/index.tsx index db621f7fb6a02..399d6ecab13de 100644 --- a/x-pack/plugins/serverless_security/public/components/side_navigation/index.tsx +++ b/x-pack/plugins/serverless_security/public/components/side_navigation/index.tsx @@ -5,22 +5,14 @@ * 2.0. */ import React from 'react'; -import { CoreStart } from '@kbn/core/public'; -import type { - SideNavComponent, - SideNavCompProps, -} from '@kbn/core-chrome-browser/src/project_navigation'; -import { ServerlessSecurityPluginStartDependencies } from '../../types'; +import type { SideNavComponent } from '@kbn/core-chrome-browser/src/project_navigation'; import { SecuritySideNavigation } from './lazy'; -import { KibanaServicesProvider } from '../../services'; +import { KibanaServicesProvider, type Services } from '../../common/services'; -export const getSecuritySideNavComponent = ( - core: CoreStart, - pluginsStart: ServerlessSecurityPluginStartDependencies -): SideNavComponent => { - return (_props: SideNavCompProps) => ( - +export const getSecuritySideNavComponent = (services: Services): SideNavComponent => { + return () => ( + ); diff --git a/x-pack/plugins/serverless_security/public/components/side_navigation/side_navigation.test.tsx b/x-pack/plugins/serverless_security/public/components/side_navigation/side_navigation.test.tsx index 5309dbf3e1295..eef0ccb8da671 100644 --- a/x-pack/plugins/serverless_security/public/components/side_navigation/side_navigation.test.tsx +++ b/x-pack/plugins/serverless_security/public/components/side_navigation/side_navigation.test.tsx @@ -10,7 +10,7 @@ import { render } from '@testing-library/react'; import { SecuritySideNavigation } from './side_navigation'; import { useSideNavItems, useSideNavSelectedId } from '../../hooks/use_side_nav_items'; import { SecurityPageName } from '@kbn/security-solution-plugin/common'; -import { KibanaServicesProvider } from '../../services.mock'; +import { KibanaServicesProvider } from '../../common/services.mock'; jest.mock('../../hooks/use_side_nav_items'); const mockUseSideNavItems = useSideNavItems as jest.Mock; diff --git a/x-pack/plugins/serverless_security/public/hooks/use_link_props.test.tsx b/x-pack/plugins/serverless_security/public/hooks/use_link_props.test.tsx index b04f19be75b8f..2a20ca97ef855 100644 --- a/x-pack/plugins/serverless_security/public/hooks/use_link_props.test.tsx +++ b/x-pack/plugins/serverless_security/public/hooks/use_link_props.test.tsx @@ -8,13 +8,13 @@ import { MouseEvent } from 'react'; import { renderHook } from '@testing-library/react-hooks'; import { APP_UI_ID, SecurityPageName } from '@kbn/security-solution-plugin/common'; -import { KibanaServicesProvider, servicesMocks } from '../services.mock'; +import { KibanaServicesProvider, servicesMocks } from '../common/services.mock'; import { useGetLinkProps, useLinkProps } from './use_link_props'; -const { getUrlForApp: mockGetUrlForApp, navigateToUrl: mockNavigateToUrl } = - servicesMocks.application; +const { getUrlForApp, navigateToUrl: mockNavigateToUrl } = servicesMocks.application; const href = '/app/security/test'; +const mockGetUrlForApp = getUrlForApp as jest.MockedFunction; mockGetUrlForApp.mockReturnValue(href); describe('useLinkProps', () => { diff --git a/x-pack/plugins/serverless_security/public/hooks/use_link_props.ts b/x-pack/plugins/serverless_security/public/hooks/use_link_props.ts index 3a1989dbdc79a..6644afb7fdf01 100644 --- a/x-pack/plugins/serverless_security/public/hooks/use_link_props.ts +++ b/x-pack/plugins/serverless_security/public/hooks/use_link_props.ts @@ -7,7 +7,7 @@ import { APP_UI_ID, type SecurityPageName } from '@kbn/security-solution-plugin/common'; import { useMemo, useCallback, type MouseEventHandler, type MouseEvent } from 'react'; -import { useKibana, type Services } from '../services'; +import { useKibana, type Services } from '../common/services'; interface LinkProps { onClick: MouseEventHandler; diff --git a/x-pack/plugins/serverless_security/public/hooks/use_nav_links.ts b/x-pack/plugins/serverless_security/public/hooks/use_nav_links.ts index 7d2b16f6cc6e8..eaa643603f86d 100644 --- a/x-pack/plugins/serverless_security/public/hooks/use_nav_links.ts +++ b/x-pack/plugins/serverless_security/public/hooks/use_nav_links.ts @@ -7,11 +7,10 @@ import { useMemo } from 'react'; import useObservable from 'react-use/lib/useObservable'; -import { useKibana } from '../services'; +import { useKibana } from '../common/services'; export const useNavLinks = () => { - const { securitySolution } = useKibana().services; - const { getNavLinks$ } = securitySolution; - const navLinks$ = useMemo(() => getNavLinks$(), [getNavLinks$]); - return useObservable(navLinks$, []); + const { getProjectNavLinks$ } = useKibana().services; + const projectNavLinks$ = useMemo(() => getProjectNavLinks$(), [getProjectNavLinks$]); + return useObservable(projectNavLinks$, []); }; diff --git a/x-pack/plugins/serverless_security/public/hooks/use_side_nav_items.test.tsx b/x-pack/plugins/serverless_security/public/hooks/use_side_nav_items.test.tsx index 38a1f4e578427..22c0e7118ec76 100644 --- a/x-pack/plugins/serverless_security/public/hooks/use_side_nav_items.test.tsx +++ b/x-pack/plugins/serverless_security/public/hooks/use_side_nav_items.test.tsx @@ -7,18 +7,15 @@ import { renderHook } from '@testing-library/react-hooks'; import { useSideNavItems, useSideNavSelectedId } from './use_side_nav_items'; -import { BehaviorSubject } from 'rxjs'; -import type { NavigationLink } from '@kbn/security-solution-plugin/public/common/links/types'; import { SecurityPageName } from '@kbn/security-solution-plugin/common'; -import { KibanaServicesProvider, servicesMocks } from '../services.mock'; +import { + KibanaServicesProvider, + servicesMocks, + mockProjectNavLinks, +} from '../common/services.mock'; jest.mock('./use_link_props'); -const mockNavLinks = jest.fn((): NavigationLink[] => []); -servicesMocks.securitySolution.getNavLinks$.mockImplementation( - () => new BehaviorSubject(mockNavLinks()) -); - const mockUseLocation = jest.fn(() => ({ pathname: '/' })); jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), @@ -36,11 +33,11 @@ describe('useSideNavItems', () => { const items = result.current; expect(items).toEqual([]); - expect(servicesMocks.securitySolution.getNavLinks$).toHaveBeenCalledTimes(1); + expect(servicesMocks.getProjectNavLinks$).toHaveBeenCalledTimes(1); }); it('should return main items', async () => { - mockNavLinks.mockReturnValueOnce([ + mockProjectNavLinks.mockReturnValueOnce([ { id: SecurityPageName.alerts, title: 'Alerts' }, { id: SecurityPageName.case, title: 'Cases' }, ]); @@ -66,7 +63,7 @@ describe('useSideNavItems', () => { }); it('should return secondary items', async () => { - mockNavLinks.mockReturnValueOnce([ + mockProjectNavLinks.mockReturnValueOnce([ { id: SecurityPageName.dashboards, title: 'Dashboards', @@ -96,7 +93,7 @@ describe('useSideNavItems', () => { }); it('should return get started link', async () => { - mockNavLinks.mockReturnValueOnce([ + mockProjectNavLinks.mockReturnValueOnce([ { id: SecurityPageName.landing, title: 'Get Started', diff --git a/x-pack/plugins/serverless_security/public/hooks/use_side_nav_items.ts b/x-pack/plugins/serverless_security/public/hooks/use_side_nav_items.ts index d352a779e9444..7ec68683eb223 100644 --- a/x-pack/plugins/serverless_security/public/hooks/use_side_nav_items.ts +++ b/x-pack/plugins/serverless_security/public/hooks/use_side_nav_items.ts @@ -8,8 +8,11 @@ import { useMemo } from 'react'; import { matchPath, useLocation } from 'react-router-dom'; import { SecurityPageName } from '@kbn/security-solution-plugin/common'; -import { SolutionSideNavItem, SolutionSideNavItemPosition } from '@kbn/security-solution-side-nav'; -import { useKibana } from '../services'; +import { + SolutionSideNavItemPosition, + type SolutionSideNavItem, +} from '@kbn/security-solution-side-nav'; +import { useKibana } from '../common/services'; import { type GetLinkProps, useGetLinkProps } from './use_link_props'; import { useNavLinks } from './use_nav_links'; diff --git a/x-pack/plugins/serverless_security/public/plugin.ts b/x-pack/plugins/serverless_security/public/plugin.ts index b93be1b16dcd4..356aea4f2c345 100644 --- a/x-pack/plugins/serverless_security/public/plugin.ts +++ b/x-pack/plugins/serverless_security/public/plugin.ts @@ -17,6 +17,9 @@ import { ServerlessSecurityPublicConfig, } from './types'; import { registerUpsellings } from './components/upselling'; +import { createServices } from './common/services'; +import { subscribeNavigationTree } from './common/navigation/navigation_tree'; +import { subscribeBreadcrumbs } from './common/navigation/breadcrumbs'; export class ServerlessSecurityPlugin implements @@ -46,13 +49,18 @@ export class ServerlessSecurityPlugin startDeps: ServerlessSecurityPluginStartDependencies ): ServerlessSecurityPluginStart { const { securitySolution, serverless } = startDeps; + const { productTypes } = this.config; + + const services = createServices(core, startDeps); securitySolution.setIsSidebarEnabled(false); - securitySolution.setGetStartedPage( - getSecurityGetStartedComponent(core, startDeps, this.config.productTypes) - ); + securitySolution.setGetStartedPage(getSecurityGetStartedComponent(services, productTypes)); + serverless.setProjectHome('/app/security'); - serverless.setSideNavComponent(getSecuritySideNavComponent(core, startDeps)); + serverless.setSideNavComponent(getSecuritySideNavComponent(services)); + + subscribeNavigationTree(services); + subscribeBreadcrumbs(services); return {}; } From 0aea720fb6002f49d90e0a06bc77eb0a13bff879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=BCrkan=20YALAMAN?= Date: Mon, 3 Jul 2023 18:24:51 +0200 Subject: [PATCH 2/3] [Enterprise Search] Change minute values to preset intervals (#161082) ## Summary https://github.com/elastic/kibana/assets/1410658/b9b7d66d-6368-4da4-90f6-2547193c07b6 Change minute values to the preset intervals 5, 10, 15 and 30 minutes presets are set. ### Checklist Delete any items that are not applicable to this PR. - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --- .../connector_cron_editor.tsx | 2 +- .../__snapshots__/cron_editor.test.tsx.snap | 800 +----------------- .../shared/cron_editor/constants.ts | 24 +- .../shared/cron_editor/cron_editor.tsx | 11 +- .../shared/cron_editor/services/cron.ts | 15 - 5 files changed, 37 insertions(+), 815 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_scheduling/connector_cron_editor.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_scheduling/connector_cron_editor.tsx index 46eccba6a4f70..9c38bdea220ca 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_scheduling/connector_cron_editor.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_scheduling/connector_cron_editor.tsx @@ -135,7 +135,7 @@ function cronToFrequency(cron: string): Frequency { if (fields.length < 4) { return 'YEAR'; } - if (fields[1] === '*' || fields[1].startsWith('*/')) { + if (fields[1] === '*' || fields[1].includes(',')) { return 'MINUTE'; } if (fields[2] === '*') { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/__snapshots__/cron_editor.test.tsx.snap b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/__snapshots__/cron_editor.test.tsx.snap index 2e22e3bfe30b0..d490f4f87057d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/__snapshots__/cron_editor.test.tsx.snap +++ b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/__snapshots__/cron_editor.test.tsx.snap @@ -3391,241 +3391,21 @@ exports[`CronEditor is rendered with a MINUTE frequency 1`] = ` minute="10" minuteOptions={ Array [ - Object { - "text": "1", - "value": "*/1", - }, - Object { - "text": "2", - "value": "*/2", - }, - Object { - "text": "3", - "value": "*/3", - }, - Object { - "text": "4", - "value": "*/4", - }, Object { "text": "5", - "value": "*/5", - }, - Object { - "text": "6", - "value": "*/6", - }, - Object { - "text": "7", - "value": "*/7", - }, - Object { - "text": "8", - "value": "*/8", - }, - Object { - "text": "9", - "value": "*/9", + "value": "0,5,10,15,20,25,30,35,40,45,50,55", }, Object { "text": "10", - "value": "*/10", - }, - Object { - "text": "11", - "value": "*/11", - }, - Object { - "text": "12", - "value": "*/12", - }, - Object { - "text": "13", - "value": "*/13", - }, - Object { - "text": "14", - "value": "*/14", + "value": "0,10,20,30,40,50", }, Object { "text": "15", - "value": "*/15", - }, - Object { - "text": "16", - "value": "*/16", - }, - Object { - "text": "17", - "value": "*/17", - }, - Object { - "text": "18", - "value": "*/18", - }, - Object { - "text": "19", - "value": "*/19", - }, - Object { - "text": "20", - "value": "*/20", - }, - Object { - "text": "21", - "value": "*/21", - }, - Object { - "text": "22", - "value": "*/22", - }, - Object { - "text": "23", - "value": "*/23", - }, - Object { - "text": "24", - "value": "*/24", - }, - Object { - "text": "25", - "value": "*/25", - }, - Object { - "text": "26", - "value": "*/26", - }, - Object { - "text": "27", - "value": "*/27", - }, - Object { - "text": "28", - "value": "*/28", - }, - Object { - "text": "29", - "value": "*/29", + "value": "0,15,30,45", }, Object { "text": "30", - "value": "*/30", - }, - Object { - "text": "31", - "value": "*/31", - }, - Object { - "text": "32", - "value": "*/32", - }, - Object { - "text": "33", - "value": "*/33", - }, - Object { - "text": "34", - "value": "*/34", - }, - Object { - "text": "35", - "value": "*/35", - }, - Object { - "text": "36", - "value": "*/36", - }, - Object { - "text": "37", - "value": "*/37", - }, - Object { - "text": "38", - "value": "*/38", - }, - Object { - "text": "39", - "value": "*/39", - }, - Object { - "text": "40", - "value": "*/40", - }, - Object { - "text": "41", - "value": "*/41", - }, - Object { - "text": "42", - "value": "*/42", - }, - Object { - "text": "43", - "value": "*/43", - }, - Object { - "text": "44", - "value": "*/44", - }, - Object { - "text": "45", - "value": "*/45", - }, - Object { - "text": "46", - "value": "*/46", - }, - Object { - "text": "47", - "value": "*/47", - }, - Object { - "text": "48", - "value": "*/48", - }, - Object { - "text": "49", - "value": "*/49", - }, - Object { - "text": "50", - "value": "*/50", - }, - Object { - "text": "51", - "value": "*/51", - }, - Object { - "text": "52", - "value": "*/52", - }, - Object { - "text": "53", - "value": "*/53", - }, - Object { - "text": "54", - "value": "*/54", - }, - Object { - "text": "55", - "value": "*/55", - }, - Object { - "text": "56", - "value": "*/56", - }, - Object { - "text": "57", - "value": "*/57", - }, - Object { - "text": "58", - "value": "*/58", - }, - Object { - "text": "59", - "value": "*/59", + "value": "0,30", }, ] } @@ -3690,241 +3470,21 @@ exports[`CronEditor is rendered with a MINUTE frequency 1`] = ` onFocus={[Function]} options={ Array [ - Object { - "text": "1", - "value": "*/1", - }, - Object { - "text": "2", - "value": "*/2", - }, - Object { - "text": "3", - "value": "*/3", - }, - Object { - "text": "4", - "value": "*/4", - }, Object { "text": "5", - "value": "*/5", - }, - Object { - "text": "6", - "value": "*/6", - }, - Object { - "text": "7", - "value": "*/7", - }, - Object { - "text": "8", - "value": "*/8", - }, - Object { - "text": "9", - "value": "*/9", + "value": "0,5,10,15,20,25,30,35,40,45,50,55", }, Object { "text": "10", - "value": "*/10", - }, - Object { - "text": "11", - "value": "*/11", - }, - Object { - "text": "12", - "value": "*/12", - }, - Object { - "text": "13", - "value": "*/13", - }, - Object { - "text": "14", - "value": "*/14", + "value": "0,10,20,30,40,50", }, Object { "text": "15", - "value": "*/15", - }, - Object { - "text": "16", - "value": "*/16", - }, - Object { - "text": "17", - "value": "*/17", - }, - Object { - "text": "18", - "value": "*/18", - }, - Object { - "text": "19", - "value": "*/19", - }, - Object { - "text": "20", - "value": "*/20", - }, - Object { - "text": "21", - "value": "*/21", - }, - Object { - "text": "22", - "value": "*/22", - }, - Object { - "text": "23", - "value": "*/23", - }, - Object { - "text": "24", - "value": "*/24", - }, - Object { - "text": "25", - "value": "*/25", - }, - Object { - "text": "26", - "value": "*/26", - }, - Object { - "text": "27", - "value": "*/27", - }, - Object { - "text": "28", - "value": "*/28", - }, - Object { - "text": "29", - "value": "*/29", + "value": "0,15,30,45", }, Object { "text": "30", - "value": "*/30", - }, - Object { - "text": "31", - "value": "*/31", - }, - Object { - "text": "32", - "value": "*/32", - }, - Object { - "text": "33", - "value": "*/33", - }, - Object { - "text": "34", - "value": "*/34", - }, - Object { - "text": "35", - "value": "*/35", - }, - Object { - "text": "36", - "value": "*/36", - }, - Object { - "text": "37", - "value": "*/37", - }, - Object { - "text": "38", - "value": "*/38", - }, - Object { - "text": "39", - "value": "*/39", - }, - Object { - "text": "40", - "value": "*/40", - }, - Object { - "text": "41", - "value": "*/41", - }, - Object { - "text": "42", - "value": "*/42", - }, - Object { - "text": "43", - "value": "*/43", - }, - Object { - "text": "44", - "value": "*/44", - }, - Object { - "text": "45", - "value": "*/45", - }, - Object { - "text": "46", - "value": "*/46", - }, - Object { - "text": "47", - "value": "*/47", - }, - Object { - "text": "48", - "value": "*/48", - }, - Object { - "text": "49", - "value": "*/49", - }, - Object { - "text": "50", - "value": "*/50", - }, - Object { - "text": "51", - "value": "*/51", - }, - Object { - "text": "52", - "value": "*/52", - }, - Object { - "text": "53", - "value": "*/53", - }, - Object { - "text": "54", - "value": "*/54", - }, - Object { - "text": "55", - "value": "*/55", - }, - Object { - "text": "56", - "value": "*/56", - }, - Object { - "text": "57", - "value": "*/57", - }, - Object { - "text": "58", - "value": "*/58", - }, - Object { - "text": "59", - "value": "*/59", + "value": "0,30", }, ] } @@ -3970,358 +3530,28 @@ exports[`CronEditor is rendered with a MINUTE frequency 1`] = ` > - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ({ - value: '*/' + value.toString(), - text: value.toString(), -})); +export const EVERY_MINUTE_OPTIONS = [ + { + text: '5', + value: '0,5,10,15,20,25,30,35,40,45,50,55', + }, + { + text: '10', + value: '0,10,20,30,40,50', + }, + { + text: '15', + value: '0,15,30,45', + }, + { + text: '30', + value: '0,30', + }, +]; export const MINUTE_OPTIONS = makeSequence(0, 59).map((value) => ({ value: value.toString(), @@ -111,7 +125,7 @@ export const frequencyToFieldsMap: Record = { export const frequencyToBaselineFieldsMap: Record = { MINUTE: { second: '0', - minute: '*/1', + minute: '0,5,10,15,20,25,30,35,40,45,50,55', hour: '*', date: '*', month: '*', diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/cron_editor.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/cron_editor.tsx index bcda5c37f33c7..ed27a69085d97 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/cron_editor.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/cron_editor.tsx @@ -29,7 +29,6 @@ import { CronMonthly } from './cron_monthly'; import { CronWeekly } from './cron_weekly'; import { CronYearly } from './cron_yearly'; import { cronExpressionToParts, cronPartsToExpression } from './services'; -import { convertFromEveryXMinute, convertToEveryXMinute } from './services/cron'; import { Frequency, Field, FieldToValueMap } from './types'; const excludeBlockListedFrequencies = ( @@ -80,20 +79,14 @@ export class CronEditor extends Component { } onChangeFrequency = (frequency: Frequency) => { - const { onChange, fieldToPreferredValueMap, frequency: oldFrequency } = this.props; + const { onChange, fieldToPreferredValueMap } = this.props; // Update fields which aren't editable with acceptable baseline values. const editableFields = Object.keys(frequencyToFieldsMap[frequency]) as Field[]; const inheritedFields = editableFields.reduce( (fieldBaselines, field) => { if (fieldToPreferredValueMap[field] != null) { - if (oldFrequency === 'MINUTE') { - fieldBaselines[field] = convertFromEveryXMinute(fieldToPreferredValueMap[field]); - } else if (frequency === 'MINUTE') { - fieldBaselines[field] = convertToEveryXMinute(fieldToPreferredValueMap[field]); - } else { - fieldBaselines[field] = fieldToPreferredValueMap[field]; - } + fieldBaselines[field] = fieldToPreferredValueMap[field]; } return fieldBaselines; }, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/services/cron.ts b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/services/cron.ts index fc2019d63c17d..542502fbcbe76 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/services/cron.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/cron_editor/services/cron.ts @@ -56,18 +56,3 @@ export function cronPartsToExpression({ }: FieldToValueMap): string { return `${second} ${minute} ${hour} ${date} ${month} ${day}`; } - -export function convertToEveryXMinute( - minute: FieldToValueMap['minute'] -): FieldToValueMap['minute'] { - if (!minute) return minute; - if (minute.startsWith('*/')) return minute; - return '*/' + minute; -} - -export function convertFromEveryXMinute( - minute: FieldToValueMap['minute'] -): FieldToValueMap['minute'] { - if (!minute) return minute; - return minute.startsWith('*/') ? minute.slice(2) : minute; -} From 6a9e8d422cc1e27089615429152b175f075790a7 Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Mon, 3 Jul 2023 12:53:46 -0400 Subject: [PATCH 3/3] [Security Solution] Update session viewer Policy permissions to use Policy specific check (#160448) ## Summary This PR updates the session viewer code to use the `canReadPolicyManagement ` permission as opposed to `canAccessEndpointManagement`. This is because `canAccessEndpointManagement` requires super user permissions while `canReadPolicyManagement` which is a more specific permission. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../session_tab_content/use_session_view.test.tsx | 2 +- .../timeline/session_tab_content/use_session_view.tsx | 6 +++--- .../public/components/session_view/index.tsx | 4 ++-- .../public/components/tty_player/index.test.tsx | 4 +--- .../public/components/tty_player/index.tsx | 10 ++++------ x-pack/plugins/session_view/public/types.ts | 2 +- 6 files changed, 12 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.test.tsx index 805cb5bf03e8e..6cf0474ead6df 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.test.tsx @@ -158,7 +158,7 @@ describe('useSessionView with active timeline and a session id and graph event i height: 1000, sessionEntityId: 'test', loadAlertDetails: mockOpenDetailFn, - canAccessEndpointManagement: false, + canReadPolicyManagement: false, }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.tsx index d2d6a82895be9..e16ac4ee9b7fd 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/session_tab_content/use_session_view.tsx @@ -264,7 +264,7 @@ export const useSessionView = ({ }, [scopeId]); const { globalFullScreen } = useGlobalFullScreen(); const { timelineFullScreen } = useTimelineFullScreen(); - const { canAccessEndpointManagement } = useUserPrivileges().endpointPrivileges; + const { canReadPolicyManagement } = useUserPrivileges().endpointPrivileges; const defaults = isTimelineScope(scopeId) ? timelineDefaults : tableDefaults; const { sessionViewConfig, activeTab } = useDeepEqualSelector((state) => ({ @@ -309,7 +309,7 @@ export const useSessionView = ({ loadAlertDetails: openEventDetailsPanel, isFullScreen: fullScreen, height: heightMinusSearchBar, - canAccessEndpointManagement, + canReadPolicyManagement, }) : null; }, [ @@ -318,7 +318,7 @@ export const useSessionView = ({ sessionView, openEventDetailsPanel, fullScreen, - canAccessEndpointManagement, + canReadPolicyManagement, ]); return { diff --git a/x-pack/plugins/session_view/public/components/session_view/index.tsx b/x-pack/plugins/session_view/public/components/session_view/index.tsx index ee4bbb40891e8..42c16c40baf5c 100644 --- a/x-pack/plugins/session_view/public/components/session_view/index.tsx +++ b/x-pack/plugins/session_view/public/components/session_view/index.tsx @@ -51,7 +51,7 @@ export const SessionView = ({ jumpToCursor, investigatedAlertId, loadAlertDetails, - canAccessEndpointManagement, + canReadPolicyManagement, }: SessionViewDeps) => { // don't engage jumpTo if jumping to session leader. if (jumpToEntityId === sessionEntityId) { @@ -435,7 +435,7 @@ export const SessionView = ({ isFullscreen={isFullScreen} onJumpToEvent={onJumpToEvent} autoSeekToEntityId={currentJumpToOutputEntityId} - canAccessEndpointManagement={canAccessEndpointManagement} + canReadPolicyManagement={canReadPolicyManagement} /> ); diff --git a/x-pack/plugins/session_view/public/components/tty_player/index.test.tsx b/x-pack/plugins/session_view/public/components/tty_player/index.test.tsx index a3a17380c8fc9..42be993d39d1d 100644 --- a/x-pack/plugins/session_view/public/components/tty_player/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/tty_player/index.test.tsx @@ -107,9 +107,7 @@ describe('TTYPlayer component', () => { }); it('renders a message warning when max_bytes exceeded with link to policies page', async () => { - renderResult = mockedContext.render( - - ); + renderResult = mockedContext.render(); await waitForApiCall(); await new Promise((r) => setTimeout(r, 10)); diff --git a/x-pack/plugins/session_view/public/components/tty_player/index.tsx b/x-pack/plugins/session_view/public/components/tty_player/index.tsx index 36d685371e023..aa85f4bd794c0 100644 --- a/x-pack/plugins/session_view/public/components/tty_player/index.tsx +++ b/x-pack/plugins/session_view/public/components/tty_player/index.tsx @@ -41,7 +41,7 @@ export interface TTYPlayerDeps { isFullscreen: boolean; onJumpToEvent(event: ProcessEvent): void; autoSeekToEntityId?: string; - canAccessEndpointManagement?: boolean; + canReadPolicyManagement?: boolean; } export const TTYPlayer = ({ @@ -53,7 +53,7 @@ export const TTYPlayer = ({ isFullscreen, onJumpToEvent, autoSeekToEntityId, - canAccessEndpointManagement, + canReadPolicyManagement, }: TTYPlayerDeps) => { const ref = useRef(null); const { ref: scrollRef, height: containerHeight = 1 } = useResizeObserver({}); @@ -71,10 +71,8 @@ export const TTYPlayer = ({ const { getUrlForApp } = useKibana().services.application; const policiesUrl = useMemo( () => - canAccessEndpointManagement - ? getUrlForApp(SECURITY_APP_ID, { path: POLICIES_PAGE_PATH }) - : '', - [canAccessEndpointManagement, getUrlForApp] + canReadPolicyManagement ? getUrlForApp(SECURITY_APP_ID, { path: POLICIES_PAGE_PATH }) : '', + [canReadPolicyManagement, getUrlForApp] ); const { search, currentLine, seekToLine } = useXtermPlayer({ diff --git a/x-pack/plugins/session_view/public/types.ts b/x-pack/plugins/session_view/public/types.ts index 3783abdfd2e8b..846d3baaa86ef 100644 --- a/x-pack/plugins/session_view/public/types.ts +++ b/x-pack/plugins/session_view/public/types.ts @@ -34,7 +34,7 @@ export interface SessionViewDeps { // Callback used when alert flyout panel is closed handleOnAlertDetailsClosed: () => void ) => void; - canAccessEndpointManagement?: boolean; + canReadPolicyManagement?: boolean; } export interface EuiTabProps {