Skip to content

Commit

Permalink
[Security Solution] Integrate default shared-ux left navigation (#167127
Browse files Browse the repository at this point in the history
)

## Summary

Main ticket: #166545

This PR integrates the shared-ux `DefaultNavigation` component in the
Security Solution project for serverless.

These changes do not replace the original Security left navigation yet,
which is still the navigation component displayed by default. In order
to render the shared-ux `DefaultNavigation` this experimental flag
should be enabled:

`xpack.securitySolutionServerless.enableExperimental:
['platformNavEnabled']`

<img width="223" alt="Captura de pantalla 2023-09-25 a les 14 00 49"
src="https://github.com/elastic/kibana/assets/17747913/91f247ce-ad9c-4093-a0f7-dbca63164b4a">


## Implementation

- Security navigation is still the default. Please enable the
`platformNavEnabled` experimental flag to render the shared navigation.
- We have two different formatters from the security navigation links
config to the navigation tree required in serverless:
- ChromeNavigationTree: registered directly to the serverless plugin for
the breadcrumbs to work when the navigation is customized with the
Security-specific nav. It will be removed after the migration to the
shared nav.
- NavigationTree: the format the shared nav uses, it already registers
the chromeNavigationTree for the breadcrumbs to the serverless plugin by
itself.
- Security plugin `deepLinks` needed to be formatted differently to make
this shared navigation work, since the `navLinkStatus: hidden` prevents
the links from being processed and displayed, this has been solved via
the `setDeepLinksFormatter` exposed on the plugin setup contract.

---------

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
semd and kibanamachine authored Oct 4, 2023
1 parent be7f34e commit 6393a91
Show file tree
Hide file tree
Showing 37 changed files with 1,156 additions and 404 deletions.
2 changes: 1 addition & 1 deletion packages/kbn-optimizer/limits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ pageLoadAssetSize:
security: 81771
securitySolution: 66738
securitySolutionEss: 16573
securitySolutionServerless: 45000
securitySolutionServerless: 62488
serverless: 16573
serverlessObservability: 68747
serverlessSearch: 71995
Expand Down
8 changes: 8 additions & 0 deletions x-pack/packages/security-solution/side_nav/panel.ts
Original file line number Diff line number Diff line change
@@ -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 { SolutionSideNavPanel } from './src/solution_side_nav_panel';
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import type { AppDeepLink, AppUpdater } from '@kbn/core/public';
import { appLinks$ } from './links';
import type { AppLinkItems } from './types';

const formatDeepLinks = (appLinks: AppLinkItems): AppDeepLink[] =>
export type DeepLinksFormatter = (appLinks: AppLinkItems) => AppDeepLink[];

const defaultDeepLinksFormatter: DeepLinksFormatter = (appLinks) =>
appLinks.map((appLink) => ({
id: appLink.id,
path: appLink.path,
Expand All @@ -23,19 +25,22 @@ const formatDeepLinks = (appLinks: AppLinkItems): AppDeepLink[] =>
...(appLink.globalSearchKeywords != null ? { keywords: appLink.globalSearchKeywords } : {}),
...(appLink.links && appLink.links?.length
? {
deepLinks: formatDeepLinks(appLink.links),
deepLinks: defaultDeepLinksFormatter(appLink.links),
}
: {}),
}));

/**
* Registers any change in appLinks to be updated in app deepLinks
*/
export const registerDeepLinksUpdater = (appUpdater$: Subject<AppUpdater>): Subscription => {
export const registerDeepLinksUpdater = (
appUpdater$: Subject<AppUpdater>,
formatter: DeepLinksFormatter = defaultDeepLinksFormatter
): Subscription => {
return appLinks$.subscribe((appLinks) => {
appUpdater$.next(() => ({
navLinkStatus: AppNavLinkStatus.hidden, // needed to prevent main security link to switch to visible after update
deepLinks: formatDeepLinks(appLinks),
deepLinks: formatter(appLinks),
}));
});
};
3 changes: 3 additions & 0 deletions x-pack/plugins/security_solution/public/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { BehaviorSubject, of } from 'rxjs';
import { UpsellingService } from '@kbn/security-solution-upselling/service';
import type { BreadcrumbsNav } from './common/breadcrumbs';
import type { NavigationLink } from './common/links/types';
import { allowedExperimentalValues } from '../common/experimental_features';
import type { PluginStart, PluginSetup, ContractStartServices } from './types';

const upselling = new UpsellingService();
Expand All @@ -23,7 +24,9 @@ export const contractStartServicesMock: ContractStartServices = {

const setupMock = (): PluginSetup => ({
resolver: jest.fn(),
experimentalFeatures: allowedExperimentalValues, // default values
setAppLinksSwitcher: jest.fn(),
setDeepLinksFormatter: jest.fn(),
setDataQualityPanelConfig: jest.fn(),
});

Expand Down
6 changes: 3 additions & 3 deletions x-pack/plugins/security_solution/public/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,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.contract = new PluginContract();
this.contract = new PluginContract(this.experimentalFeatures);
this.telemetry = new TelemetryService();
this.storage = new Storage(window.localStorage);
}
Expand Down Expand Up @@ -518,9 +518,9 @@ 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 { upsellingService, appLinksSwitcher } = this.contract;
const { upsellingService, appLinksSwitcher, deepLinksFormatter } = this.contract;

registerDeepLinksUpdater(this.appUpdater$);
registerDeepLinksUpdater(this.appUpdater$, deepLinksFormatter);

const baseLinksPermissions: LinksPermissions = {
experimentalFeatures: this.experimentalFeatures,
Expand Down
9 changes: 8 additions & 1 deletion x-pack/plugins/security_solution/public/plugin_contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { UpsellingService } from '@kbn/security-solution-upselling/service';
import type { ContractStartServices, PluginSetup, PluginStart } from './types';
import type { DataQualityPanelConfig } from './overview/types';
import type { AppLinksSwitcher } from './common/links';
import type { DeepLinksFormatter } from './common/links/deep_links';
import type { ExperimentalFeatures } from '../common/experimental_features';
import { navLinks$ } from './common/links/nav_links';
import { breadcrumbsNav$ } from './common/breadcrumbs';
import { ContractComponentsService } from './contract_components';
Expand All @@ -21,9 +23,10 @@ export class PluginContract {
public upsellingService: UpsellingService;
public extraRoutes$: BehaviorSubject<RouteProps[]>;
public appLinksSwitcher: AppLinksSwitcher;
public deepLinksFormatter?: DeepLinksFormatter;
public dataQualityPanelConfig?: DataQualityPanelConfig;

constructor() {
constructor(private readonly experimentalFeatures: ExperimentalFeatures) {
this.extraRoutes$ = new BehaviorSubject<RouteProps[]>([]);
this.isSidebarEnabled$ = new BehaviorSubject<boolean>(true);
this.componentsService = new ContractComponentsService();
Expand All @@ -44,9 +47,13 @@ export class PluginContract {
public getSetupContract(): PluginSetup {
return {
resolver: lazyResolver,
experimentalFeatures: { ...this.experimentalFeatures },
setAppLinksSwitcher: (appLinksSwitcher) => {
this.appLinksSwitcher = appLinksSwitcher;
},
setDeepLinksFormatter: (deepLinksFormatter) => {
this.deepLinksFormatter = deepLinksFormatter;
},
setDataQualityPanelConfig: (dataQualityPanelConfig) => {
this.dataQualityPanelConfig = dataQualityPanelConfig;
},
Expand Down
4 changes: 4 additions & 0 deletions x-pack/plugins/security_solution/public/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ import type { TelemetryClientStart } from './common/lib/telemetry';
import type { Dashboards } from './dashboards';
import type { BreadcrumbsNav } from './common/breadcrumbs/types';
import type { TopValuesPopoverService } from './app/components/top_values_popover/top_values_popover_service';
import type { ExperimentalFeatures } from '../common/experimental_features';
import type { DeepLinksFormatter } from './common/links/deep_links';
import type { DataQualityPanelConfig } from './overview/types';
import type { SetComponents, GetComponent$ } from './contract_components';

Expand Down Expand Up @@ -176,7 +178,9 @@ export type StartServices = CoreStart &

export interface PluginSetup {
resolver: () => Promise<ResolverPluginSetup>;
experimentalFeatures: ExperimentalFeatures;
setAppLinksSwitcher: (appLinksSwitcher: AppLinksSwitcher) => void;
setDeepLinksFormatter: (deepLinksFormatter: DeepLinksFormatter) => void;
setDataQualityPanelConfig: (dataQualityPanelConfig: DataQualityPanelConfig) => void;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export class AppFeaturesService {
}

public setAppFeaturesConfigurator(configurator: AppFeaturesConfigurator) {
const securityAppFeaturesConfig = configurator.security(this.experimentalFeatures);
const securityAppFeaturesConfig = configurator.security();
this.securityAppFeatures.setConfig(securityAppFeaturesConfig);

const casesAppFeaturesConfig = configurator.cases();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@ import type {
CasesSubFeatureId,
AssistantSubFeatureId,
} from '@kbn/security-solution-features/keys';
import type { ExperimentalFeatures } from '../../../common';

export interface AppFeaturesConfigurator {
security: (experimentalFlags: ExperimentalFeatures) => AppFeaturesConfig<SecuritySubFeatureId>;
security: () => AppFeaturesConfig<SecuritySubFeatureId>;
cases: () => AppFeaturesConfig<CasesSubFeatureId>;
securityAssistant: () => AppFeaturesConfig<AssistantSubFeatureId>;
}
1 change: 1 addition & 0 deletions x-pack/plugins/security_solution/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,7 @@ export class Plugin implements ISecuritySolutionPlugin {
return {
setAppFeaturesConfigurator:
appFeaturesService.setAppFeaturesConfigurator.bind(appFeaturesService),
experimentalFeatures: { ...config.experimentalFeatures },
};
}

Expand Down
5 changes: 5 additions & 0 deletions x-pack/plugins/security_solution/server/plugin_contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import type { SharePluginStart } from '@kbn/share-plugin/server';
import type { GuidedOnboardingPluginSetup } from '@kbn/guided-onboarding-plugin/server';
import type { PluginSetup as UnifiedSearchServerPluginSetup } from '@kbn/unified-search-plugin/server';
import type { AppFeaturesService } from './lib/app_features_service/app_features_service';
import type { ExperimentalFeatures } from '../common';

export interface SecuritySolutionPluginSetupDependencies {
alerting: AlertingPluginSetup;
Expand Down Expand Up @@ -87,6 +88,10 @@ export interface SecuritySolutionPluginSetup {
* Sets the configurations for app features that are available to the Security Solution
*/
setAppFeaturesConfigurator: AppFeaturesService['setAppFeaturesConfigurator'];
/**
* The security solution generic experimental features
*/
experimentalFeatures: ExperimentalFeatures;
}

// eslint-disable-next-line @typescript-eslint/no-empty-interface
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { ExperimentalFeatures } from '@kbn/security-solution-plugin/common';
import type {
AppFeatureKeys,
AppFeatureKibanaConfig,
Expand All @@ -24,10 +23,7 @@ import {
} from '@kbn/security-solution-features/privileges';

export const getSecurityAppFeaturesConfigurator =
(enabledAppFeatureKeys: AppFeatureKeys) =>
(
_: ExperimentalFeatures // currently un-used, but left here as a convenience for possible future use
): AppFeaturesSecurityConfig => {
(enabledAppFeatureKeys: AppFeatureKeys) => (): AppFeaturesSecurityConfig => {
return createEnabledAppFeaturesConfigMap(securityAppFeaturesConfig, enabledAppFeatureKeys);
};

Expand Down
23 changes: 23 additions & 0 deletions x-pack/plugins/security_solution_serverless/common/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,26 @@ export const developerConfigSchema = schema.object({
});

export type DeveloperConfig = TypeOf<typeof developerConfigSchema>;

export const configSchema = schema.object({
enabled: schema.boolean({ defaultValue: false }),
developer: developerConfigSchema,
productTypes,
/**
* For internal use. A list of string values (comma delimited) that will enable experimental
* type of functionality that is not yet released. Valid values for this settings need to
* be defined in:
* `x-pack/plugins/security_solution_serverless/common/experimental_features.ts`
* under the `allowedExperimentalValues` object
*
* @example
* xpack.securitySolutionServerless.enableExperimental:
* - someCrazyFeature
* - someEvenCrazierFeature
*/
enableExperimental: schema.arrayOf(schema.string(), {
defaultValue: () => [],
}),
});

export type ServerlessSecurityConfigSchema = TypeOf<typeof configSchema>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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 { ExperimentalFeatures as GenericExperimentalFeatures } from '@kbn/security-solution-plugin/common';

export type ServerlessExperimentalFeatures = Record<
keyof typeof allowedExperimentalValues,
boolean
>;

/**
* A list of allowed values that can be used in `xpack.securitySolutionServerless.enableExperimental`.
* This object is then used to validate and parse the value entered.
*/
export const allowedExperimentalValues = Object.freeze({
/**
* Enables the use of the of the product navigation from shared-ux package in the Security Solution app
*/
platformNavEnabled: false,
});

type ServerlessExperimentalConfigKeys = Array<keyof ServerlessExperimentalFeatures>;
type Mutable<T> = { -readonly [P in keyof T]: T[P] };

const allowedKeys = Object.keys(
allowedExperimentalValues
) as Readonly<ServerlessExperimentalConfigKeys>;

export type ExperimentalFeatures = ServerlessExperimentalFeatures & GenericExperimentalFeatures;
/**
* Parses the string value used in `xpack.securitySolutionServerless.enableExperimental` kibana configuration,
* which should be a string of values delimited by a comma (`,`)
* The generic experimental features are merged with the serverless values to ensure they are available
*
* @param configValue
* @throws SecuritySolutionInvalidExperimentalValue
*/
export const parseExperimentalConfigValue = (
configValue: string[],
genericExperimentalFeatures: GenericExperimentalFeatures
): { features: ExperimentalFeatures; invalid: string[]; duplicated: string[] } => {
const enabledFeatures: Mutable<Partial<ExperimentalFeatures>> = {};
const invalidKeys: string[] = [];
const duplicatedKeys: string[] = [];

for (const value of configValue) {
if (genericExperimentalFeatures[value as keyof GenericExperimentalFeatures] != null) {
duplicatedKeys.push(value);
} else if (!allowedKeys.includes(value as keyof ServerlessExperimentalFeatures)) {
invalidKeys.push(value);
} else {
enabledFeatures[value as keyof ServerlessExperimentalFeatures] = true;
}
}

return {
features: {
...genericExperimentalFeatures,
...allowedExperimentalValues,
...enabledFeatures,
},
invalid: invalidKeys,
duplicated: duplicatedKeys,
};
};

export const getExperimentalAllowedValues = (): string[] => [...allowedKeys];
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@ import { managementPluginMock } from '@kbn/management-plugin/public/mocks';
import { cloudMock } from '@kbn/cloud-plugin/public/mocks';
import type { ProjectNavigationLink } from '../../../navigation/links/types';
import type { Services } from '..';
import { allowedExperimentalValues as genericAllowedExperimentalValues } from '@kbn/security-solution-plugin/common';
import { allowedExperimentalValues } from '../../../../common/experimental_features';

export const mockProjectNavLinks = jest.fn((): ProjectNavigationLink[] => []);

export const mockServices: Services = {
...coreMock.createStart(),
experimentalFeatures: { ...allowedExperimentalValues, ...genericAllowedExperimentalValues },
serverless: serverlessMock.createStart(),
security: securityMock.createStart(),
securitySolution: securitySolutionMock.createStart(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import type { CoreStart } from '@kbn/core/public';
import type { ExperimentalFeatures } from '../../../common/experimental_features';
import { createProjectNavLinks$ } from '../../navigation/links/nav_links';
import type { SecuritySolutionServerlessPluginStartDeps } from '../../types';
import type { Services } from './types';
Expand All @@ -16,9 +17,20 @@ import type { Services } from './types';
* */
export const createServices = (
core: CoreStart,
pluginsStart: SecuritySolutionServerlessPluginStartDeps
pluginsStart: SecuritySolutionServerlessPluginStartDeps,
experimentalFeatures: ExperimentalFeatures
): Services => {
const { securitySolution, cloud } = pluginsStart;
const projectNavLinks$ = createProjectNavLinks$(securitySolution.getNavLinks$(), core, cloud);
return { ...core, ...pluginsStart, getProjectNavLinks$: () => projectNavLinks$ };
const projectNavLinks$ = createProjectNavLinks$(
securitySolution.getNavLinks$(),
core,
cloud,
experimentalFeatures
);
return {
...core,
...pluginsStart,
experimentalFeatures,
getProjectNavLinks$: () => projectNavLinks$,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
*/

import type { CoreStart } from '@kbn/core/public';
import type { ExperimentalFeatures } from '../../../common/experimental_features';
import type { ProjectNavLinks } from '../../navigation/links/types';
import type { SecuritySolutionServerlessPluginStartDeps } from '../../types';

export interface InternalServices {
experimentalFeatures: ExperimentalFeatures;
getProjectNavLinks$: () => ProjectNavLinks;
}
export type Services = CoreStart & SecuritySolutionServerlessPluginStartDeps & InternalServices;
Loading

0 comments on commit 6393a91

Please sign in to comment.