mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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

---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
a81287f10c
commit
44c7091507
68 changed files with 1266 additions and 1031 deletions
|
@ -35,7 +35,8 @@
|
|||
"home",
|
||||
"taskManager",
|
||||
"usageCollection",
|
||||
"spaces"
|
||||
"spaces",
|
||||
"serverless",
|
||||
],
|
||||
"requiredBundles": [],
|
||||
"extraPublicDirs": [
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
|
|
|
@ -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<CoreStart, 'application' | 'http' | 'theme'>;
|
||||
type GlobalServices = Pick<CoreStart, 'application' | 'http' | 'theme'> &
|
||||
Pick<CasesPluginStart, 'serverless'>;
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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]);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -67,6 +67,7 @@
|
|||
"@kbn/core-lifecycle-browser",
|
||||
"@kbn/core-saved-objects-api-server-mocks",
|
||||
"@kbn/core-theme-browser",
|
||||
"@kbn/serverless",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
};
|
7
x-pack/plugins/ess_security/public/breadcrumbs/index.ts
Normal file
7
x-pack/plugins/ess_security/public/breadcrumbs/index.ts
Normal file
|
@ -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';
|
|
@ -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: ['<rootDir>/x-pack/plugins/ess_security/public/common'],
|
||||
testMatch: ['<rootDir>/x-pack/plugins/ess_security/public/common/**/*.test.{js,mjs,ts,tsx}'],
|
||||
coverageDirectory:
|
||||
'<rootDir>/target/kibana-coverage/jest/x-pack/plugins/ess_security/public/common',
|
||||
coverageReporters: ['text', 'html'],
|
||||
collectCoverageFrom: [
|
||||
'<rootDir>/x-pack/plugins/ess_security/public/common/**/*.{ts,tsx}',
|
||||
'!<rootDir>/x-pack/plugins/ess_security/public/common/*.test.{ts,tsx}',
|
||||
'!<rootDir>/x-pack/plugins/ess_security/public/common/{__test__,__snapshots__,__examples__,*mock*,tests,test_helpers,integration_tests,types}/**/*',
|
||||
'!<rootDir>/x-pack/plugins/ess_security/public/common/*mock*.{ts,tsx}',
|
||||
'!<rootDir>/x-pack/plugins/ess_security/public/common/*.test.{ts,tsx}',
|
||||
'!<rootDir>/x-pack/plugins/ess_security/public/common/*.d.ts',
|
||||
'!<rootDir>/x-pack/plugins/ess_security/public/common/*.config.ts',
|
||||
'!<rootDir>/x-pack/plugins/ess_security/public/common/index.{js,ts,tsx}',
|
||||
],
|
||||
};
|
|
@ -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: ['<rootDir>/x-pack/plugins/ess_security/public/get_started'],
|
||||
coverageDirectory:
|
||||
'<rootDir>/target/kibana-coverage/jest/x-pack/plugins/ess_security/public/get_started',
|
||||
coverageReporters: ['text', 'html'],
|
||||
collectCoverageFrom: ['<rootDir>/x-pack/plugins/ess_security/public/get_started/**/*.{ts,tsx}'],
|
||||
};
|
|
@ -8,7 +8,7 @@ module.exports = {
|
|||
preset: '@kbn/test',
|
||||
rootDir: '../../../..',
|
||||
/** all nested directories have their own Jest config file */
|
||||
testMatch: ['<rootDir>/x-pack/plugins/ess_security/public/*.test.{js,mjs,ts,tsx}'],
|
||||
testMatch: ['<rootDir>/x-pack/plugins/ess_security/public/**/*.test.{js,mjs,ts,tsx}'],
|
||||
roots: ['<rootDir>/x-pack/plugins/ess_security/public'],
|
||||
coverageDirectory: '<rootDir>/target/kibana-coverage/jest/x-pack/plugins/ess_security/public',
|
||||
coverageReporters: ['text', 'html'],
|
||||
|
|
|
@ -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 {};
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<BreadcrumbsNav>({
|
||||
leading: [],
|
||||
trailing: [],
|
||||
});
|
||||
|
||||
// The observable can be exposed by the plugin contract.
|
||||
export const breadcrumbsNav$ = breadcrumbsNavUpdater$.asObservable();
|
||||
|
||||
export const updateBreadcrumbsNav = (breadcrumbsNav: BreadcrumbsNav) => {
|
||||
breadcrumbsNavUpdater$.next(breadcrumbsNav);
|
||||
};
|
|
@ -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';
|
|
@ -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[];
|
||||
}
|
|
@ -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];
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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';
|
||||
|
|
|
@ -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 [];
|
||||
};
|
|
@ -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<T extends RouteSpyState = RouteSpyState> = (
|
||||
spyState: T,
|
||||
getSecuritySolutionUrl: GetSecuritySolutionUrl
|
||||
) => ChromeBreadcrumb[];
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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 });
|
||||
};
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 [];
|
||||
};
|
|
@ -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, string> = {
|
|||
[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<AlertDetailRouteSpyState> = (
|
||||
params,
|
||||
getSecuritySolutionUrl
|
||||
) => {
|
||||
let breadcrumb: ChromeBreadcrumb[] = [];
|
||||
|
||||
if (params.detailName != null) {
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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',
|
||||
|
|
|
@ -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) {
|
|
@ -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, string> = {
|
||||
[HostsTableType.hosts]: i18n.NAVIGATION_ALL_HOSTS_TITLE,
|
||||
|
@ -29,10 +26,15 @@ const TabNameMappedToI18nKey: Record<HostsTableType, string> = {
|
|||
[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<HostRouteSpyState> = (
|
||||
params,
|
||||
getSecuritySolutionUrl
|
||||
) => {
|
||||
let breadcrumb: ChromeBreadcrumb[] = [];
|
||||
|
||||
if (params.detailName != null) {
|
|
@ -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'}
|
||||
|
|
|
@ -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<HostDetailsTabsProps>(
|
|||
skip: isInitializing || filterQuery === undefined,
|
||||
setQuery,
|
||||
startDate: from,
|
||||
type,
|
||||
type: HostsType.details,
|
||||
indexPattern,
|
||||
indexNames,
|
||||
hostName: detailName,
|
||||
|
|
|
@ -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<HostDetailsProps> = ({ detailName, hostDeta
|
|||
to={to}
|
||||
from={from}
|
||||
detailName={detailName}
|
||||
type={type}
|
||||
type={HostsType.details}
|
||||
setQuery={setQuery}
|
||||
filterQuery={stringifiedAdditionalFilters}
|
||||
hostDetailsPagePath={hostDetailsPagePath}
|
||||
|
|
|
@ -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 | NetworkRouteType, string> = {
|
||||
[NetworkDetailsRouteType.events]: i18n.NAVIGATION_EVENTS_TITLE,
|
||||
[NetworkDetailsRouteType.anomalies]: i18n.NAVIGATION_ANOMALIES_TITLE,
|
||||
|
@ -28,11 +26,15 @@ const TabNameMappedToI18nKey: Record<NetworkDetailsRouteType | NetworkRouteType,
|
|||
[NetworkDetailsRouteType.tls]: i18n.NAVIGATION_TLS_TITLE,
|
||||
[NetworkRouteType.dns]: i18n.NAVIGATION_DNS_TITLE,
|
||||
};
|
||||
|
||||
export const getTrailingBreadcrumbs = (
|
||||
params: NetworkRouteSpyState,
|
||||
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<NetworkRouteSpyState> = (
|
||||
params,
|
||||
getSecuritySolutionUrl
|
||||
) => {
|
||||
let breadcrumb: ChromeBreadcrumb[] = [];
|
||||
|
||||
if (params.detailName != null) {
|
|
@ -58,8 +58,6 @@ import {
|
|||
SecurityCellActionsTrigger,
|
||||
} from '../../../../common/components/cell_actions';
|
||||
|
||||
export { getTrailingBreadcrumbs } from './utils';
|
||||
|
||||
const NetworkDetailsManage = manageQuery(IpOverview);
|
||||
|
||||
const NetworkDetailsComponent: React.FC = () => {
|
||||
|
|
|
@ -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, string> = {
|
||||
[UsersTableType.allUsers]: i18n.NAVIGATION_ALL_USERS_TITLE,
|
||||
|
@ -28,10 +25,15 @@ const TabNameMappedToI18nKey: Record<UsersTableType, string> = {
|
|||
[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<UsersRouteSpyState> = (
|
||||
params,
|
||||
getSecuritySolutionUrl
|
||||
) => {
|
||||
let breadcrumb: ChromeBreadcrumb[] = [];
|
||||
|
||||
if (params.detailName != null) {
|
|
@ -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<UsersDetailsProps> = ({
|
|||
userDetailFilter={usersDetailsPageFilters}
|
||||
setQuery={setQuery}
|
||||
to={to}
|
||||
type={type}
|
||||
type={UsersType.details}
|
||||
usersDetailsPagePath={usersDetailsPagePath}
|
||||
/>
|
||||
</SecuritySolutionPageWrapper>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<NavigationLink[]>([])),
|
||||
setIsSidebarEnabled: jest.fn(),
|
||||
setGetStartedPage: jest.fn(),
|
||||
getBreadcrumbsNav$: jest.fn(
|
||||
() => new BehaviorSubject<BreadcrumbsNav>({ leading: [], trailing: [] })
|
||||
),
|
||||
});
|
||||
|
||||
export const securitySolutionMock = {
|
||||
|
|
|
@ -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<PluginSetup, PluginStart, SetupPlugins, StartPlugins> {
|
||||
/**
|
||||
|
@ -76,12 +75,10 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
*/
|
||||
readonly prebuiltRulesPackageVersion?: string;
|
||||
private config: SecuritySolutionUiConfigType;
|
||||
private contract: PluginContract;
|
||||
private telemetry: TelemetryService;
|
||||
|
||||
readonly experimentalFeatures: ExperimentalFeatures;
|
||||
private upsellingService: UpsellingService;
|
||||
private isSidebarEnabled$: BehaviorSubject<boolean>;
|
||||
private getStartedComponent$: BehaviorSubject<React.ComponentType | null>;
|
||||
|
||||
constructor(private readonly initializerContext: PluginInitializerContext) {
|
||||
this.config = this.initializerContext.config.get<SecuritySolutionUiConfigType>();
|
||||
|
@ -91,9 +88,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
this.kibanaVersion = initializerContext.env.packageInfo.version;
|
||||
this.kibanaBranch = initializerContext.env.packageInfo.branch;
|
||||
this.prebuiltRulesPackageVersion = this.config.prebuiltRulesPackageVersion;
|
||||
this.isSidebarEnabled$ = new BehaviorSubject<boolean>(true);
|
||||
this.getStartedComponent$ = new BehaviorSubject<React.ComponentType | null>(null);
|
||||
this.upsellingService = new UpsellingService();
|
||||
this.contract = new PluginContract();
|
||||
this.telemetry = new TelemetryService();
|
||||
}
|
||||
private appUpdater$ = new Subject<AppUpdater>();
|
||||
|
@ -158,6 +153,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
const services: StartServices = {
|
||||
...coreStart,
|
||||
...startPlugins,
|
||||
...this.contract.getStartServices(),
|
||||
apm,
|
||||
savedObjectsTagging: savedObjectsTaggingOss.getTaggingApi(),
|
||||
storage: this.storage,
|
||||
|
@ -168,9 +164,6 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
getPluginWrapper: () => 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<PluginSetup, PluginStart, SetupPlugins, S
|
|||
},
|
||||
});
|
||||
|
||||
return {
|
||||
resolver: 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();
|
||||
},
|
||||
upselling: this.upsellingService,
|
||||
};
|
||||
return this.contract.getSetupContract();
|
||||
}
|
||||
|
||||
public start(core: CoreStart, plugins: StartPlugins): PluginStart {
|
||||
|
@ -310,19 +291,12 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
// Not using await to prevent blocking start execution
|
||||
this.registerAppLinks(core, plugins);
|
||||
|
||||
return {
|
||||
getNavLinks$: () => 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<PluginSetup, PluginStart, SetupPlugins, S
|
|||
async registerAppLinks(core: CoreStart, plugins: StartPlugins) {
|
||||
const { links, getFilteredLinks } = await this.lazyApplicationLinks();
|
||||
const { license$ } = plugins.licensing;
|
||||
const upselling = this.contract.upsellingService;
|
||||
|
||||
registerDeepLinksUpdater(this.appUpdater$);
|
||||
|
||||
license$.subscribe(async (license) => {
|
||||
const linksPermissions: LinksPermissions = {
|
||||
experimentalFeatures: this.experimentalFeatures,
|
||||
upselling: this.upsellingService,
|
||||
upselling,
|
||||
capabilities: core.application.capabilities,
|
||||
};
|
||||
|
||||
|
|
67
x-pack/plugins/security_solution/public/plugin_contract.ts
Normal file
67
x-pack/plugins/security_solution/public/plugin_contract.ts
Normal file
|
@ -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<boolean>;
|
||||
public getStartedComponent$: BehaviorSubject<React.ComponentType | null>;
|
||||
public upsellingService: UpsellingService;
|
||||
|
||||
constructor() {
|
||||
this.isSidebarEnabled$ = new BehaviorSubject<boolean>(true);
|
||||
this.getStartedComponent$ = new BehaviorSubject<React.ComponentType | null>(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();
|
||||
};
|
|
@ -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<boolean>;
|
||||
getStartedComponent$: Observable<React.ComponentType | null>;
|
||||
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<boolean>;
|
||||
getStartedComponent$: BehaviorSubject<React.ComponentType | null>;
|
||||
upselling: UpsellingService;
|
||||
telemetry: TelemetryClientStart;
|
||||
};
|
||||
|
||||
|
@ -158,6 +163,7 @@ export interface PluginStart {
|
|||
getNavLinks$: () => Observable<NavigationLink[]>;
|
||||
setIsSidebarEnabled: (isSidebarEnabled: boolean) => void;
|
||||
setGetStartedPage: (getStartedComponent: React.ComponentType) => void;
|
||||
getBreadcrumbsNav$: () => Observable<BreadcrumbsNav>;
|
||||
}
|
||||
|
||||
export interface AppObservableLibs {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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: ['<rootDir>/x-pack/plugins/serverless_security/public/common'],
|
||||
testMatch: [
|
||||
'<rootDir>/x-pack/plugins/serverless_security/public/common/**/*.test.{js,mjs,ts,tsx}',
|
||||
],
|
||||
coverageDirectory:
|
||||
'<rootDir>/target/kibana-coverage/jest/x-pack/plugins/serverless_security/public/common',
|
||||
coverageReporters: ['text', 'html'],
|
||||
collectCoverageFrom: [
|
||||
'<rootDir>/x-pack/plugins/serverless_security/public/common/**/*.{ts,tsx}',
|
||||
'!<rootDir>/x-pack/plugins/serverless_security/public/common/*.test.{ts,tsx}',
|
||||
'!<rootDir>/x-pack/plugins/serverless_security/public/common/{__test__,__snapshots__,__examples__,*mock*,tests,test_helpers,integration_tests,types}/**/*',
|
||||
'!<rootDir>/x-pack/plugins/serverless_security/public/common/*mock*.{ts,tsx}',
|
||||
'!<rootDir>/x-pack/plugins/serverless_security/public/common/*.test.{ts,tsx}',
|
||||
'!<rootDir>/x-pack/plugins/serverless_security/public/common/*.d.ts',
|
||||
'!<rootDir>/x-pack/plugins/serverless_security/public/common/*.config.ts',
|
||||
'!<rootDir>/x-pack/plugins/serverless_security/public/common/index.{js,ts,tsx}',
|
||||
],
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
};
|
|
@ -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';
|
|
@ -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<NavigationLink[]>): 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;
|
|
@ -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<ProjectNavigationLink[]>;
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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>([
|
||||
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<ChromeProjectNavigationNode[]>((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;
|
||||
}, []);
|
|
@ -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 }) => (
|
|
@ -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 <KibanaContextProvider services={services}>{children}</KibanaContextProvider>;
|
||||
};
|
||||
|
||||
export const useKibana = () => useKibanaReact<Services>();
|
||||
|
||||
export const createServices = (
|
||||
core: CoreStart,
|
||||
pluginsStart: ServerlessSecurityPluginStartDependencies
|
||||
): Services => {
|
||||
const { securitySolution } = pluginsStart;
|
||||
const projectNavLinks$ = getProjectNavLinks$(securitySolution.getNavLinks$());
|
||||
return { ...core, ...pluginsStart, getProjectNavLinks$: () => projectNavLinks$ };
|
||||
};
|
|
@ -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 () => (
|
||||
<KibanaServicesProvider core={core} pluginsStart={pluginsStart}>
|
||||
<KibanaServicesProvider services={services}>
|
||||
<GetStarted productTypes={productTypes} />
|
||||
</KibanaServicesProvider>
|
||||
);
|
||||
|
|
|
@ -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', () => ({
|
||||
|
|
|
@ -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) => (
|
||||
<KibanaServicesProvider core={core} pluginsStart={pluginsStart}>
|
||||
export const getSecuritySideNavComponent = (services: Services): SideNavComponent => {
|
||||
return () => (
|
||||
<KibanaServicesProvider services={services}>
|
||||
<SecuritySideNavigation />
|
||||
</KibanaServicesProvider>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<typeof getUrlForApp>;
|
||||
mockGetUrlForApp.mockReturnValue(href);
|
||||
|
||||
describe('useLinkProps', () => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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$, []);
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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 {};
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue