[Security Solution] Integrate new navigation in stateful (#179971)

## Summary

issue: https://github.com/elastic/kibana/issues/179162

This PR brings the new navigation, with the solution-centric IA
(Information Architecture), to the ESS (stateful) Security Solution.

To do so, the implementation of the `navigationTree$`, which was
previously only implemented in serverless, has been integrated inside
the generic "security_solution" plugin, so now it is available for ESS
and serverless offerings.

In ESS users can still choose the navigation version, so we have to
temporarily keep supporting both, the classic and new navigation
implementations. After the rollout, the classic navigation components
will be removed and the unified links architecture should be reassessed.
The issue for the cleaning:
https://github.com/elastic/kibana/issues/179572

### Rollout

The new solutions navigation will not be available for customers on
8.14, it will only be enabled for internal Elastic users (via
Lauchdarkly), who will have the ability to opt out of it from their
profile menu. We'll collect feedback and telemetry and address any bugs
or improvements (together with the Kibana platform team). The plan is to
start making it available to customers in 8.15.

### Testing

Unless we add the _kibana.yml_ configurations to enable the new
navigation, the regular classic navigation will be displayed, it should
keep working the same way without any change. The new landing pages
(`Assets`, `Investigations`, `Machine Learning`,...), that exist only
when using the new navigation, should not be accessible using the
classic version.

To enable the new navigation add the following _kibana.yml_ configs:
```
xpack.cloud_integrations.experiments.enabled: true
xpack.cloud_integrations.experiments.flag_overrides:
  "navigation.solutionNavEnabled": true

xpack.cloud.id: "ftr_fake_cloud_id:aGVsbG8uY29tOjQ0MyRFUzEyM2FiYyRrYm4xMjNhYmM="
xpack.cloud.base_url: "https://cloud.elastic.co"
xpack.cloud.deployment_url: "/deployments/deploymentId"
```
And enable the advanced setting


![image](07e8952d-5bd5-4700-8105-7732f08de28e)

### Screenshots

The app switcher:

<img width="293" alt="app switcher"
src="0a638b8f-fdc0-4d1a-b8d3-607e487215f4">

---

New Assets landing page:

<img width="549" alt="assets landing"
src="17bc8a94-02b4-4996-b9f5-8731ba81ac43">

---

For `Stack Management` we set the nav panel flyout and the cards landing
page, this is temporary until a decision on how to show Stack Management
links is made:

<img width="954" alt="stack management"
src="27ce6534-0508-4804-b224-8dc409042825">

---

The switch to go back to the classic nav is in the profile menu at the
top-right corner:


![nav_switch](f547b051-4924-42da-b12f-e308a4da5868)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Søren Louv-Jansen <soren.louv@elastic.co>
Co-authored-by: Vitalii Dmyterko <92328789+vitaliidm@users.noreply.github.com>
Co-authored-by: jennypavlova <dzheni.pavlova@elastic.co>
Co-authored-by: Katerina <aikaterini.patticha@elastic.co>
Co-authored-by: Sébastien Loix <sebastien.loix@elastic.co>
Co-authored-by: Kurt <kc13greiner@users.noreply.github.com>
Co-authored-by: Justin Kambic <jk@elastic.co>
Co-authored-by: Julia Bardi <90178898+juliaElastic@users.noreply.github.com>
Co-authored-by: Paul Tavares <56442535+paul-tavares@users.noreply.github.com>
Co-authored-by: Nathan Reese <reese.nathan@elastic.co>
Co-authored-by: Dzmitry Lemechko <dzmitry.lemechko@elastic.co>
Co-authored-by: Marshall Main <55718608+marshallmain@users.noreply.github.com>
Co-authored-by: Milton Hultgren <milton.hultgren@elastic.co>
This commit is contained in:
Sergi Massaneda 2024-04-12 10:29:47 +02:00 committed by GitHub
parent 00ee0dc4d1
commit 95d1d8bf72
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
127 changed files with 2852 additions and 2076 deletions

View file

@ -21,9 +21,9 @@ xpack.securitySolutionServerless.productTypes:
]
xpack.securitySolution.offeringSettings: {
ILMEnabled: false, # Index Lifecycle Management (ILM) functionalities disabled, not supported by serverless Elasticsearch
ESQLEnabled: false, # ES|QL disabled, not supported by serverless Elasticsearch
}
ILMEnabled: false, # Index Lifecycle Management (ILM) functionalities disabled, not supported by serverless Elasticsearch
ESQLEnabled: false, # ES|QL disabled, not supported by serverless Elasticsearch
}
newsfeed.enabled: true

View file

@ -41,7 +41,7 @@ import type {
NavigationPublicSetupDependencies,
NavigationPublicStartDependencies,
ConfigSchema,
SolutionNavigation,
AddSolutionNavigationArg,
SolutionType,
} from './types';
import { TopNavMenuExtensionsRegistry, createTopNav } from './top_nav_menu';
@ -182,9 +182,11 @@ export class NavigationPublicPlugin
// Keep track of the solution navigation enabled state
let isSolutionNavEnabled = false;
this.isSolutionNavEnabled$.pipe(takeUntil(this.stop$)).subscribe((_isSolutionNavEnabled) => {
isSolutionNavEnabled = _isSolutionNavEnabled;
});
isSolutionNavExperiementEnabled$
.pipe(takeUntil(this.stop$))
.subscribe((_isSolutionNavEnabled) => {
isSolutionNavEnabled = _isSolutionNavEnabled;
});
return {
ui: {
@ -192,14 +194,7 @@ export class NavigationPublicPlugin
AggregateQueryTopNavMenu: createTopNav(unifiedSearch, extensions),
createTopNavWithCustomContext: createCustomTopNav,
},
addSolutionNavigation: (
solutionNavigation: Omit<SolutionNavigation, 'sideNavComponent'> & {
/** Data test subj for the side navigation */
dataTestSubj?: string;
/** Panel content provider for the side navigation */
panelContentProvider?: PanelContentProvider;
}
) => {
addSolutionNavigation: (solutionNavigation) => {
if (!isSolutionNavEnabled) return;
return this.addSolutionNavigation(solutionNavigation);
},
@ -268,19 +263,10 @@ export class NavigationPublicPlugin
);
}
private addSolutionNavigation(
solutionNavigation: SolutionNavigation & {
/** Data test subj for the side navigation */
dataTestSubj?: string;
/** Panel content provider for the side navigation */
panelContentProvider?: PanelContentProvider;
}
) {
private addSolutionNavigation(solutionNavigation: AddSolutionNavigationArg) {
if (!this.coreStart) throw new Error('coreStart is not available');
const { dataTestSubj, panelContentProvider, ...rest } = solutionNavigation;
const sideNavComponent =
solutionNavigation.sideNavComponent ??
this.getSideNavComponent({ dataTestSubj, panelContentProvider });
const sideNavComponent = this.getSideNavComponent({ dataTestSubj, panelContentProvider });
const { project } = this.coreStart.chrome as InternalChromeStart;
project.updateSolutionNavigations({
[solutionNavigation.id]: { ...rest, sideNavComponent },

View file

@ -14,6 +14,7 @@ import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public';
import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/public';
import type { CloudExperimentsPluginStart } from '@kbn/cloud-experiments-plugin/common';
import { PanelContentProvider } from '@kbn/shared-ux-chrome-navigation';
import { TopNavMenuProps, TopNavMenuExtensionsRegistrySetup, createTopNav } from './top_nav_menu';
import type { RegisteredTopNavMenuData } from './top_nav_menu/top_nav_menu_data';
@ -22,6 +23,12 @@ export interface NavigationPublicSetup {
}
export type SolutionNavigation = Omit<SolutionNavigationDefinition, 'sideNavComponentGetter'>;
export type AddSolutionNavigationArg = Omit<SolutionNavigation, 'sideNavComponent'> & {
/** Data test subj for the side navigation */
dataTestSubj?: string;
/** Panel content provider for the side navigation */
panelContentProvider?: PanelContentProvider;
};
export interface NavigationPublicStart {
ui: {
@ -33,7 +40,7 @@ export interface NavigationPublicStart {
) => ReturnType<typeof createTopNav>;
};
/** Add a solution navigation to the header nav switcher. */
addSolutionNavigation: (solutionNavigation: SolutionNavigation) => void;
addSolutionNavigation: (solutionNavigationAgg: AddSolutionNavigationArg) => void;
/** Flag to indicate if the solution navigation is enabled.*/
isSolutionNavEnabled$: Observable<boolean>;
}

View file

@ -13,5 +13,10 @@ export {
} from './src/navigation';
export type { GetAppUrl, NavigateTo } from './src/navigation';
export { NavigationProvider } from './src/context';
export { SecurityPageName, LinkCategoryType } from './src/constants';
export {
SecurityPageName,
ExternalPageName,
LinkCategoryType,
SECURITY_UI_APP_ID,
} from './src/constants';
export * from './src/types';

View file

@ -15,3 +15,88 @@ export enum LinkCategoryType {
accordion = 'accordion',
separator = 'separator',
}
/**
* External (non-Security) page names that need to be linked in the Security navigation.
* Format: `<pluginId>:<deepLinkId>/<path>`.
*
* `pluginId`: is the id of the plugin that owns the deep link
*
* `deepLinkId`: is the id of the deep link inside the plugin.
* Keep empty for the root page of the plugin, e.g. `osquery:`
*
* `path`: is the path to append to the plugin and deep link.
* This is optional and only needed if the path is not registered in the plugin's `deepLinks`. e.g. `integrations:/browse/security`
* The path should not be used for links displayed in the main left navigation, since highlighting won't work.
**/
export enum ExternalPageName {
// Discover
discover = 'discover:',
// Osquery
osquery = 'osquery:',
// Analytics
maps = 'maps:',
visualize = 'visualize:',
// Machine Learning
// Ref: packages/default-nav/ml/default_navigation.ts
mlOverview = 'ml:overview',
mlNotifications = 'ml:notifications',
mlMemoryUsage = 'ml:memoryUsage',
mlAnomalyDetection = 'ml:anomalyDetection',
mlAnomalyExplorer = 'ml:anomalyExplorer',
mlSingleMetricViewer = 'ml:singleMetricViewer',
mlSettings = 'ml:settings',
mlDataFrameAnalytics = 'ml:dataFrameAnalytics',
mlResultExplorer = 'ml:resultExplorer',
mlAnalyticsMap = 'ml:analyticsMap',
mlNodesOverview = 'ml:nodesOverview',
mlNodes = 'ml:nodes',
mlFileUpload = 'ml:fileUpload',
mlIndexDataVisualizer = 'ml:indexDataVisualizer',
mlDataDrift = 'ml:dataDrift',
mlExplainLogRateSpikes = 'ml:logRateAnalysis',
mlLogPatternAnalysis = 'ml:logPatternAnalysis',
mlChangePointDetections = 'ml:changePointDetections',
// Dev Tools
// Ref: packages/default-nav/devtools/default_navigation.ts
devTools = 'dev_tools:',
// Fleet
// Ref: x-pack/plugins/fleet/public/deep_links.ts
fleet = 'fleet:',
fleetAgents = 'fleet:agents',
fleetPolicies = 'fleet:policies',
fleetEnrollmentTokens = 'fleet:enrollment_tokens',
fleetUninstallTokens = 'fleet:uninstall_tokens',
fleetDataStreams = 'fleet:data_streams',
fleetSettings = 'fleet:settings',
// Integrations
// No deepLinkId registered, using path for the security search
integrationsSecurity = 'integrations:/browse/security',
// Management
// Ref: packages/default-nav/management/default_navigation.ts
management = 'management:',
managementIngestPipelines = 'management:ingest_pipelines',
managementPipelines = 'management:pipelines',
managementIndexManagement = 'management:index_management',
managementTransforms = 'management:transform',
managementMaintenanceWindows = 'management:maintenanceWindows',
managementTriggersActions = 'management:triggersActions',
managementCases = 'management:cases',
managementTriggersActionsConnectors = 'management:triggersActionsConnectors',
managementReporting = 'management:reporting',
managementJobsListLink = 'management:jobsListLink',
managementDataViews = 'management:dataViews',
managementObjects = 'management:objects',
managementApiKeys = 'management:api_keys',
managementTags = 'management:tags',
managementFiles = 'management:filesManagement',
managementSpaces = 'management:spaces',
managementSettings = 'management:settings',
// Cloud UI
// These are links to Cloud UI outside Kibana
// Special Format: <cloud>:<cloudUrlKey>
// cloudUrlKey Ref: x-pack/plugins/security_solution_serverless/public/navigation/links/util.ts
cloudUsersAndRoles = 'cloud:usersAndRoles',
cloudBilling = 'cloud:billing',
cloudPerformance = 'cloud:performance',
}

View file

@ -6,7 +6,6 @@
*/
import React from 'react';
import {
EuiLink,
EuiFlexGroup,
EuiFlexItem,
useEuiTheme,
@ -33,24 +32,11 @@ export const LandingLink: React.FC<LandingLinkProps> = React.memo(function Landi
children,
...rest
}) {
if (item.externalUrl != null) {
// Link to outside Kibana
const linkProps: EuiLinkAnchorProps = {
target: '_blank',
external: true,
href: item.externalUrl,
...(onLinkClick && !item.disabled && { onClick: () => onLinkClick(item.id) }),
...rest,
};
return <EuiLink {...linkProps}>{children}</EuiLink>;
} else {
// Kibana link
const linkProps = {
...getKibanaLinkProps({ item, urlState, onLinkClick }),
...rest,
};
return <LinkAnchor {...linkProps}>{children}</LinkAnchor>;
}
const linkProps = {
...getKibanaLinkProps({ item, urlState, onLinkClick }),
...rest,
};
return <LinkAnchor {...linkProps}>{children}</LinkAnchor>;
});
interface LandingLinksProps {

View file

@ -12,7 +12,6 @@ export interface NavigationLink<T extends string = string> {
categories?: LinkCategories<T>;
description?: string;
disabled?: boolean;
externalUrl?: string;
id: T;
landingIcon?: IconType;
landingImage?: string;
@ -21,6 +20,7 @@ export interface NavigationLink<T extends string = string> {
sideNavIcon?: IconType;
skipUrlState?: boolean;
unauthorized?: boolean;
isFooterLink?: boolean;
isBeta?: boolean;
betaOptions?: {
text: string;

View file

@ -6,10 +6,6 @@
*/
export interface ConfigSettings {
/**
* Security solution internal side navigation enabled
*/
sideNavEnabled: boolean;
/**
* Index Lifecycle Management (ILM) feature enabled.
*/
@ -25,7 +21,6 @@ export interface ConfigSettings {
* This object is then used to validate and parse the value entered.
*/
export const defaultSettings: ConfigSettings = Object.freeze({
sideNavEnabled: true,
ILMEnabled: true,
ESQLEnabled: true,
});

View file

@ -105,6 +105,10 @@ export const NETWORK_PATH = '/network' as const;
export const MANAGEMENT_PATH = '/administration' as const;
export const COVERAGE_OVERVIEW_PATH = '/rules_coverage_overview' as const;
export const THREAT_INTELLIGENCE_PATH = '/threat_intelligence' as const;
export const INVESTIGATIONS_PATH = '/investigations' as const;
export const MACHINE_LEARNING_PATH = '/ml' as const;
export const ASSETS_PATH = '/assets' as const;
export const CLOUD_DEFEND_PATH = '/cloud_defend' as const;
export const ENDPOINTS_PATH = `${MANAGEMENT_PATH}/endpoints` as const;
export const POLICIES_PATH = `${MANAGEMENT_PATH}/policy` as const;
export const TRUSTED_APPS_PATH = `${MANAGEMENT_PATH}/trusted_apps` as const;

View file

@ -8,10 +8,8 @@
import React from 'react';
import type { RouteProps } from 'react-router-dom';
import { Redirect } from 'react-router-dom';
import { EuiLoadingElastic } from '@elastic/eui';
import { Routes, Route } from '@kbn/shared-ux-router';
import type { Capabilities } from '@kbn/core/public';
import useObservable from 'react-use/lib/useObservable';
import { CASES_FEATURE_ID, CASES_PATH, LANDING_PATH, SERVER_APP_ID } from '../../common/constants';
import { NotFoundPage } from './404';
import type { StartServices } from '../types';
@ -21,33 +19,19 @@ export interface AppRoutesProps {
subPluginRoutes: RouteProps[];
}
export const AppRoutes: React.FC<AppRoutesProps> = ({ services, subPluginRoutes }) => {
const extraRoutes = useObservable(services.extraRoutes$, null);
export const AppRoutes: React.FC<AppRoutesProps> = React.memo(({ services, subPluginRoutes }) => (
<Routes>
{subPluginRoutes.map((route, index) => {
return <Route key={`route-${index}`} {...route} />;
})}
<Route>
<RedirectRoute capabilities={services.application.capabilities} />
</Route>
</Routes>
));
AppRoutes.displayName = 'AppRoutes';
return (
<Routes>
{subPluginRoutes.map((route, index) => {
return <Route key={`route-${index}`} {...route} />;
})}
{extraRoutes?.map((route, index) => {
return <Route key={`extra-route-${index}`} {...route} />;
}) ?? (
// `extraRoutes$` have array value (defaults to []), the first render we receive `null` from the useObservable initialization.
// We need to wait until we receive the array value to prevent the fallback redirection to the landing page.
<Route>
<EuiLoadingElastic size="xl" style={{ display: 'flex', margin: 'auto' }} />
</Route>
)}
<Route>
<RedirectRoute capabilities={services.application.capabilities} />
</Route>
</Routes>
);
};
export const RedirectRoute = React.memo<{ capabilities: Capabilities }>(function RedirectRoute({
capabilities,
}) {
export const RedirectRoute = React.memo<{ capabilities: Capabilities }>(({ capabilities }) => {
if (capabilities[SERVER_APP_ID].show === true) {
return <Redirect to={LANDING_PATH} />;
}
@ -56,3 +40,4 @@ export const RedirectRoute = React.memo<{ capabilities: Capabilities }>(function
}
return <NotFoundPage />;
});
RedirectRoute.displayName = 'RedirectRoute';

View file

@ -6,16 +6,15 @@
*/
import { i18n } from '@kbn/i18n';
import type { LinkCategory, SeparatorLinkCategory } from '@kbn/security-solution-navigation';
import {
SecurityPageName,
ExternalPageName,
LinkCategoryType,
type LinkCategory,
type SeparatorLinkCategory,
SecurityPageName,
} from '@kbn/security-solution-navigation';
import { ExternalPageName } from './links/constants';
import type { ProjectPageName } from './links/types';
import type { SolutionPageName } from '../../common/links';
export const CATEGORIES: Array<SeparatorLinkCategory<ProjectPageName>> = [
export const CATEGORIES: Array<SeparatorLinkCategory<SolutionPageName>> = [
{
type: LinkCategoryType.separator,
linkIds: [ExternalPageName.discover, SecurityPageName.dashboards],
@ -39,7 +38,7 @@ export const CATEGORIES: Array<SeparatorLinkCategory<ProjectPageName>> = [
},
{
type: LinkCategoryType.separator,
linkIds: [ExternalPageName.fleet, SecurityPageName.assets],
linkIds: [SecurityPageName.assets],
},
{
type: LinkCategoryType.separator,
@ -52,23 +51,17 @@ export const CATEGORIES: Array<SeparatorLinkCategory<ProjectPageName>> = [
},
];
export const FOOTER_CATEGORIES: Array<LinkCategory<ProjectPageName>> = [
export const FOOTER_CATEGORIES: Array<LinkCategory<SolutionPageName>> = [
{
type: LinkCategoryType.separator,
linkIds: [SecurityPageName.landing, ExternalPageName.devTools],
},
{
type: LinkCategoryType.accordion,
label: i18n.translate('xpack.securitySolutionServerless.nav.projectSettings.title', {
defaultMessage: 'Project settings',
label: i18n.translate('xpack.securitySolution.navCategory.management.title', {
defaultMessage: 'Management',
}),
iconType: 'gear',
linkIds: [
ExternalPageName.management,
ExternalPageName.integrationsSecurity,
ExternalPageName.cloudUsersAndRoles,
ExternalPageName.cloudPerformance,
ExternalPageName.cloudBilling,
],
linkIds: [ExternalPageName.management, ExternalPageName.integrationsSecurity],
},
];

View file

@ -4,4 +4,5 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export { subscribeBreadcrumbs } from './breadcrumbs';
export { getSolutionNavigation } from './solution_navigation';

View file

@ -5,48 +5,43 @@
* 2.0.
*/
import type {
AppLinksSwitcher,
LinkItem,
} from '@kbn/security-solution-plugin/public/common/links/types';
import { SecurityPageName } from '@kbn/security-solution-navigation';
import { cloneDeep, find, remove } from 'lodash';
import type { AppLinkItems, LinkItem } from '../../../common/links/types';
import { createInvestigationsLinkFromTimeline } from './sections/investigations_links';
import { mlAppLink } from './sections/ml_links';
import { createAssetsLinkFromManage } from './sections/assets_links';
import { createProjectSettingsLinksFromManage } from './sections/project_settings_links';
import { createSettingsLinksFromManage } from './sections/settings_links';
// This function is called by the security_solution plugin to alter the app links
// that will be registered to the Security Solution application on Serverless projects.
// that will be registered to the Security Solution application using the new "solution-centric" IA.
// The capabilities filtering is done after this function is called by the security_solution plugin.
export const projectAppLinksSwitcher: AppLinksSwitcher = (appLinks) => {
const projectAppLinks = cloneDeep(appLinks) as LinkItem[];
// TODO: remove after rollout https://github.com/elastic/kibana/issues/179572
export const solutionAppLinksSwitcher = (appLinks: AppLinkItems): AppLinkItems => {
const solutionAppLinks = cloneDeep(appLinks) as LinkItem[];
// Remove timeline link
const [timelineLinkItem] = remove(projectAppLinks, { id: SecurityPageName.timelines });
const [timelineLinkItem] = remove(solutionAppLinks, { id: SecurityPageName.timelines });
if (timelineLinkItem) {
// Add investigations link
projectAppLinks.push(createInvestigationsLinkFromTimeline(timelineLinkItem));
solutionAppLinks.push(createInvestigationsLinkFromTimeline(timelineLinkItem));
}
// Remove data quality dashboard link
const dashboardLinkItem = find(projectAppLinks, { id: SecurityPageName.dashboards });
const dashboardLinkItem = find(solutionAppLinks, { id: SecurityPageName.dashboards });
if (dashboardLinkItem && dashboardLinkItem.links) {
remove(dashboardLinkItem.links, { id: SecurityPageName.dataQuality });
}
// Remove manage link
const [manageLinkItem] = remove(projectAppLinks, { id: SecurityPageName.administration });
const [manageLinkItem] = remove(solutionAppLinks, { id: SecurityPageName.administration });
if (manageLinkItem) {
// Add assets link
projectAppLinks.push(createAssetsLinkFromManage(manageLinkItem));
// Add entity analytics link if exists
projectAppLinks.push(...createProjectSettingsLinksFromManage(manageLinkItem));
solutionAppLinks.push(createAssetsLinkFromManage(manageLinkItem));
solutionAppLinks.push(...createSettingsLinksFromManage(manageLinkItem));
}
// Add ML link
projectAppLinks.push(mlAppLink);
solutionAppLinks.push(mlAppLink);
return projectAppLinks;
return Object.freeze(solutionAppLinks);
};

View file

@ -5,31 +5,30 @@
* 2.0.
*/
import type { ChromeNavLink } from '@kbn/core/public';
import { APP_UI_ID } from '@kbn/security-solution-plugin/common';
import { APP_UI_ID } from '../../../../common';
import type { NavigationLink } from '@kbn/security-solution-navigation';
import { SecurityPageName } from '@kbn/security-solution-navigation';
import { createProjectNavLinks$ } from './nav_links';
import { ExternalPageName, SecurityPageName } from '@kbn/security-solution-navigation';
import { coreMock } from '@kbn/core/public/mocks';
import { createSolutionNavLinks$ } from './nav_links';
import type { Observable } from 'rxjs';
import { BehaviorSubject, firstValueFrom, take } from 'rxjs';
import { mockServices } from '../../common/services/__mocks__/services.mock';
import { mlNavCategories, mlNavLinks } from './sections/ml_links';
import { assetsNavLinks } from './sections/assets_links';
import { ExternalPageName } from './constants';
import { investigationsNavLinks } from './sections/investigations_links';
import { isCloudLink } from './util';
const mockCloudStart = mockServices.cloud;
const mockChromeNavLinks = jest.fn((): ChromeNavLink[] => []);
const mockChromeGetNavLinks = jest.fn(() => new BehaviorSubject(mockChromeNavLinks()));
const mockChromeNavLinksHas = jest.fn((id: string): boolean =>
mockChromeNavLinks().some((link) => link.id === id)
);
const coreStartMock = coreMock.createStart();
const testServices = {
...mockServices,
...coreStartMock,
chrome: {
...mockServices.chrome,
...coreStartMock.chrome,
navLinks: {
...mockServices.chrome.navLinks,
...coreStartMock.chrome.navLinks,
has: mockChromeNavLinksHas,
getNavLinks$: mockChromeGetNavLinks,
},
@ -65,19 +64,10 @@ const devToolsChromeNavLink: ChromeNavLink = {
};
const createTestProjectNavLinks = async (
testSecurityNavLinks$: Observable<Array<NavigationLink<SecurityPageName>>>,
{ filterCloudLinks = true }: { filterCloudLinks?: boolean } = {}
testSecurityNavLinks$: Observable<Array<NavigationLink<SecurityPageName>>>
) => {
const projectNavLinks$ = createProjectNavLinks$(
testSecurityNavLinks$,
testServices,
mockCloudStart
);
const value = await firstValueFrom(projectNavLinks$.pipe(take(1)));
if (filterCloudLinks) {
return value.filter((link) => !isCloudLink(link.id));
}
return value;
const projectNavLinks$ = createSolutionNavLinks$(testSecurityNavLinks$, testServices);
return firstValueFrom(projectNavLinks$.pipe(take(1)));
};
describe('getProjectNavLinks', () => {
@ -164,34 +154,17 @@ describe('getProjectNavLinks', () => {
);
});
it('should add project settings links', async () => {
it('should add settings links', async () => {
mockChromeNavLinksHas.mockReturnValue(true); // all links exist
const testSecurityNavLinks$ = new BehaviorSubject([link1]);
const value = await createTestProjectNavLinks(testSecurityNavLinks$, {
filterCloudLinks: false,
});
const value = await createTestProjectNavLinks(testSecurityNavLinks$);
expect(value).toEqual(
expect.arrayContaining([
link1,
expect.objectContaining({ id: ExternalPageName.management }),
expect.objectContaining({ id: ExternalPageName.integrationsSecurity }),
expect.objectContaining({ id: ExternalPageName.cloudUsersAndRoles }),
expect.objectContaining({ id: ExternalPageName.cloudBilling }),
])
);
});
it('should process cloud links', async () => {
mockChromeNavLinksHas.mockReturnValue(true); // all links exist
const testSecurityNavLinks$ = new BehaviorSubject([link1]);
const value = await createTestProjectNavLinks(testSecurityNavLinks$, {
filterCloudLinks: false,
});
const cloudLinks = value.filter(({ id }) => isCloudLink(id));
expect(cloudLinks.length > 0).toBe(true);
expect(cloudLinks.every((cloudLink) => cloudLink.externalUrl)).toBe(true);
});
});

View file

@ -9,21 +9,19 @@ import { map, combineLatest, skipWhile, debounceTime, type Observable } from 'rx
import type { ChromeNavLinks, CoreStart } from '@kbn/core/public';
import { SecurityPageName, type NavigationLink } from '@kbn/security-solution-navigation';
import { isSecurityId } from '@kbn/security-solution-navigation/links';
import type { CloudStart } from '@kbn/cloud-plugin/public';
import { assetsNavLinks } from './sections/assets_links';
import { mlNavCategories, mlNavLinks } from './sections/ml_links';
import { projectSettingsNavLinks } from './sections/project_settings_links';
import { settingsNavLinks } from './sections/settings_links';
import { devToolsNavLink } from './sections/dev_tools_links';
import { discoverNavLink } from './sections/discover_links';
import type { ProjectNavigationLink } from './types';
import { getCloudLinkKey, getCloudUrl, getNavLinkIdFromProjectPageName, isCloudLink } from './util';
import type { SolutionNavLink } from '../../../common/links';
import { getNavLinkIdFromSolutionPageName } from '../util';
import { investigationsNavLinks } from './sections/investigations_links';
export const createProjectNavLinks$ = (
export const createSolutionNavLinks$ = (
securityNavLinks$: Observable<Array<NavigationLink<SecurityPageName>>>,
core: CoreStart,
cloud: CloudStart
): Observable<ProjectNavigationLink[]> => {
core: CoreStart
): Observable<SolutionNavLink[]> => {
const { chrome } = core;
return combineLatest([securityNavLinks$, chrome.navLinks.getNavLinks$()]).pipe(
debounceTime(100), // avoid multiple calls in a short period of time
@ -31,7 +29,7 @@ export const createProjectNavLinks$ = (
([securityNavLinks, chromeNavLinks]) =>
securityNavLinks.length === 0 || chromeNavLinks.length === 0 // skip if not initialized
),
map(([securityNavLinks]) => processNavLinks(securityNavLinks, chrome.navLinks, cloud))
map(([securityNavLinks]) => processNavLinks(securityNavLinks, chrome.navLinks))
);
};
@ -41,51 +39,50 @@ export const createProjectNavLinks$ = (
*/
const processNavLinks = (
securityNavLinks: Array<NavigationLink<SecurityPageName>>,
chromeNavLinks: ChromeNavLinks,
cloud: CloudStart
): ProjectNavigationLink[] => {
const projectNavLinks: ProjectNavigationLink[] = [...securityNavLinks];
chromeNavLinks: ChromeNavLinks
): SolutionNavLink[] => {
const solutionNavLinks: SolutionNavLink[] = [...securityNavLinks];
// Discover. just pushing it
projectNavLinks.push(discoverNavLink);
solutionNavLinks.push(discoverNavLink);
// Investigations. injecting external sub-links and categories definition to the landing
const investigationsLinkIndex = projectNavLinks.findIndex(
const investigationsLinkIndex = solutionNavLinks.findIndex(
({ id }) => id === SecurityPageName.investigations
);
if (investigationsLinkIndex !== -1) {
const investigationNavLink = projectNavLinks[investigationsLinkIndex];
projectNavLinks[investigationsLinkIndex] = {
const investigationNavLink = solutionNavLinks[investigationsLinkIndex];
solutionNavLinks[investigationsLinkIndex] = {
...investigationNavLink,
links: [...(investigationNavLink.links ?? []), ...investigationsNavLinks],
};
}
// ML. injecting external sub-links and categories definition to the landing
const mlLinkIndex = projectNavLinks.findIndex(({ id }) => id === SecurityPageName.mlLanding);
const mlLinkIndex = solutionNavLinks.findIndex(({ id }) => id === SecurityPageName.mlLanding);
if (mlLinkIndex !== -1) {
projectNavLinks[mlLinkIndex] = {
...projectNavLinks[mlLinkIndex],
solutionNavLinks[mlLinkIndex] = {
...solutionNavLinks[mlLinkIndex],
categories: mlNavCategories,
links: mlNavLinks,
};
}
// Assets, adding fleet external sub-links
const assetsLinkIndex = projectNavLinks.findIndex(({ id }) => id === SecurityPageName.assets);
const assetsLinkIndex = solutionNavLinks.findIndex(({ id }) => id === SecurityPageName.assets);
if (assetsLinkIndex !== -1) {
const assetsNavLink = projectNavLinks[assetsLinkIndex];
projectNavLinks[assetsLinkIndex] = {
const assetsNavLink = solutionNavLinks[assetsLinkIndex];
solutionNavLinks[assetsLinkIndex] = {
...assetsNavLink,
links: [...assetsNavLinks, ...(assetsNavLink.links ?? [])], // adds fleet to the existing (endpoints and cloud) links
};
}
// Dev Tools. just pushing it
projectNavLinks.push(devToolsNavLink);
projectNavLinks.push(...projectSettingsNavLinks);
solutionNavLinks.push(devToolsNavLink);
solutionNavLinks.push(...settingsNavLinks);
return processCloudLinks(filterDisabled(projectNavLinks, chromeNavLinks), cloud);
return filterDisabled(solutionNavLinks, chromeNavLinks);
};
/**
@ -93,13 +90,13 @@ const processNavLinks = (
* Internal Security links are already filtered by the security_solution plugin appLinks.
*/
const filterDisabled = (
projectNavLinks: ProjectNavigationLink[],
solutionNavLinks: SolutionNavLink[],
chromeNavLinks: ChromeNavLinks
): ProjectNavigationLink[] => {
return projectNavLinks.reduce<ProjectNavigationLink[]>((filteredNavLinks, navLink) => {
): SolutionNavLink[] => {
return solutionNavLinks.reduce<SolutionNavLink[]>((filteredNavLinks, navLink) => {
const { id, links } = navLink;
if (!isSecurityId(id) && !isCloudLink(id)) {
const navLinkId = getNavLinkIdFromProjectPageName(id);
if (!isSecurityId(id)) {
const navLinkId = getNavLinkIdFromSolutionPageName(id);
if (!chromeNavLinks.has(navLinkId)) {
return filteredNavLinks;
}
@ -112,23 +109,3 @@ const filterDisabled = (
return filteredNavLinks;
}, []);
};
const processCloudLinks = (
links: ProjectNavigationLink[],
cloud: CloudStart
): ProjectNavigationLink[] => {
return links.map((link) => {
const extraProps: Partial<ProjectNavigationLink> = {};
if (isCloudLink(link.id)) {
const externalUrl = getCloudUrl(getCloudLinkKey(link.id), cloud);
extraProps.externalUrl = externalUrl || '#'; // fallback to # if empty, should only happen in dev
}
if (link.links) {
extraProps.links = processCloudLinks(link.links, cloud);
}
return {
...link,
...extraProps,
};
});
};

View file

@ -5,19 +5,19 @@
* 2.0.
*/
import { SecurityPageName } from '@kbn/security-solution-navigation';
import { SERVER_APP_ID } from '@kbn/security-solution-plugin/common';
import type { LinkItem } from '@kbn/security-solution-plugin/public';
import { ExternalPageName, SecurityPagePath } from '../constants';
import type { ProjectNavigationLink } from '../types';
import { IconEcctlLazy, IconFleetLazy } from '../../../common/lazy_icons';
import { SecurityPageName, ExternalPageName } from '@kbn/security-solution-navigation';
import { ASSETS_PATH, CLOUD_DEFEND_PATH } from '../../../../../common/constants';
import { SERVER_APP_ID } from '../../../../../common';
import type { LinkItem } from '../../../../common/links/types';
import type { SolutionNavLink } from '../../../../common/links';
import { IconEcctlLazy, IconFleetLazy } from './lazy_icons';
import * as i18n from './assets_translations';
// appLinks configures the Security Solution pages links
const assetsAppLink: LinkItem = {
id: SecurityPageName.assets,
title: i18n.ASSETS_TITLE,
path: SecurityPagePath[SecurityPageName.assets],
path: ASSETS_PATH,
capabilities: [`${SERVER_APP_ID}.show`],
hideTimeline: true,
skipUrlState: true,
@ -29,7 +29,7 @@ const assetsCloudDefendAppLink: LinkItem = {
id: SecurityPageName.cloudDefend,
title: i18n.CLOUD_DEFEND_TITLE,
description: i18n.CLOUD_DEFEND_DESCRIPTION,
path: SecurityPagePath[SecurityPageName.cloudDefend],
path: CLOUD_DEFEND_PATH,
capabilities: [`${SERVER_APP_ID}.show`],
landingIcon: IconEcctlLazy,
isBeta: true,
@ -69,7 +69,7 @@ export const createAssetsLinkFromManage = (manageLink: LinkItem): LinkItem => {
};
// navLinks define the navigation links for the Security Solution pages and External pages as well
export const assetsNavLinks: ProjectNavigationLink[] = [
export const assetsNavLinks: SolutionNavLink[] = [
{
id: ExternalPageName.fleet,
title: i18n.FLEET_TITLE,

View file

@ -7,70 +7,64 @@
import { i18n } from '@kbn/i18n';
export const ASSETS_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.assets.title',
{
defaultMessage: 'Assets',
}
);
export const ASSETS_TITLE = i18n.translate('xpack.securitySolution.navLinks.assets.title', {
defaultMessage: 'Assets',
});
export const CLOUD_DEFEND_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.assets.cloud_defend.title',
'xpack.securitySolution.navLinks.assets.cloud_defend.title',
{
defaultMessage: 'Cloud',
}
);
export const CLOUD_DEFEND_DESCRIPTION = i18n.translate(
'xpack.securitySolutionServerless.navLinks.assets.cloud_defend.description',
'xpack.securitySolution.navLinks.assets.cloud_defend.description',
{
defaultMessage: 'Cloud hosts running Elastic Defend',
}
);
export const FLEET_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.assets.fleet.title',
{
defaultMessage: 'Fleet',
}
);
export const FLEET_TITLE = i18n.translate('xpack.securitySolution.navLinks.assets.fleet.title', {
defaultMessage: 'Fleet',
});
export const FLEET_DESCRIPTION = i18n.translate(
'xpack.securitySolutionServerless.navLinks.assets.fleet.description',
'xpack.securitySolution.navLinks.assets.fleet.description',
{
defaultMessage: 'Centralized management for Elastic Agents',
}
);
export const FLEET_AGENTS_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.assets.fleet.agents.title',
'xpack.securitySolution.navLinks.assets.fleet.agents.title',
{
defaultMessage: 'Agents',
}
);
export const FLEET_POLICIES_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.assets.fleet.policies.title',
'xpack.securitySolution.navLinks.assets.fleet.policies.title',
{
defaultMessage: 'Policies',
}
);
export const FLEET_ENROLLMENT_TOKENS_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.assets.fleet.enrollmentTokens.title',
'xpack.securitySolution.navLinks.assets.fleet.enrollmentTokens.title',
{
defaultMessage: 'Enrollment tokens',
}
);
export const FLEET_UNINSTALL_TOKENS_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.assets.fleet.uninstallTokens.title',
'xpack.securitySolution.navLinks.assets.fleet.uninstallTokens.title',
{
defaultMessage: 'Uninstall tokens',
}
);
export const FLEET_DATA_STREAMS_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.assets.fleet.dataStreams.title',
'xpack.securitySolution.navLinks.assets.fleet.dataStreams.title',
{
defaultMessage: 'Data streams',
}
);
export const FLEET_SETTINGS_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.assets.fleet.settings.title',
'xpack.securitySolution.navLinks.assets.fleet.settings.title',
{
defaultMessage: 'Settings',
}

View file

@ -5,12 +5,13 @@
* 2.0.
*/
import { ExternalPageName } from '../constants';
import type { ProjectNavigationLink } from '../types';
import { ExternalPageName } from '@kbn/security-solution-navigation';
import type { SolutionNavLink } from '../../../../common/links';
import { DEV_TOOLS_TITLE } from './dev_tools_translations';
export const devToolsNavLink: ProjectNavigationLink = {
export const devToolsNavLink: SolutionNavLink = {
id: ExternalPageName.devTools,
title: DEV_TOOLS_TITLE,
sideNavIcon: 'editorCodeBlock',
isFooterLink: true,
};

View file

@ -7,9 +7,6 @@
import { i18n } from '@kbn/i18n';
export const DEV_TOOLS_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.devTools.title',
{
defaultMessage: 'Developer tools',
}
);
export const DEV_TOOLS_TITLE = i18n.translate('xpack.securitySolution.navLinks.devTools.title', {
defaultMessage: 'Developer tools',
});

View file

@ -5,11 +5,11 @@
* 2.0.
*/
import { ExternalPageName } from '../constants';
import type { ProjectNavigationLink } from '../types';
import { ExternalPageName } from '@kbn/security-solution-navigation';
import type { SolutionNavLink } from '../../../../common/links';
import { DISCOVER_TITLE } from './discover_translations';
export const discoverNavLink: ProjectNavigationLink = {
export const discoverNavLink: SolutionNavLink = {
id: ExternalPageName.discover,
title: DISCOVER_TITLE,
};

View file

@ -7,9 +7,6 @@
import { i18n } from '@kbn/i18n';
export const DISCOVER_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.discover.title',
{
defaultMessage: 'Discover',
}
);
export const DISCOVER_TITLE = i18n.translate('xpack.securitySolution.navLinks.discover.title', {
defaultMessage: 'Discover',
});

View file

@ -0,0 +1,58 @@
/*
* 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 { SVGProps } from 'react';
import React from 'react';
export const IconChartArrow: React.FC<SVGProps<SVGSVGElement>> = (props) => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M31.4185 4.11481L20.4558 15.0775L12.5084 7.13003L1.61832 18.0201L0.204102 16.6059L12.5084 4.3016L20.4558 12.249L30.0043 2.7006L31.4185 4.11481Z"
fill="#00BFB3"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.01855 31V22H5.01855V31H3.01855Z"
fill="#535966"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M19.0186 31V22H21.0186V31H19.0186Z"
fill="#535966"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M11.0186 31V15H13.0186V31H11.0186Z"
fill="#535966"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M27.0186 31V15H29.0186V31H27.0186Z"
fill="#535966"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M29.9998 4.05764L24.9883 3.99993L25.0113 2.00006L31.9998 2.08054V9H29.9998V4.05764Z"
fill="#535966"
/>
</svg>
);
// eslint-disable-next-line import/no-default-export
export default IconChartArrow;

View file

@ -0,0 +1,58 @@
/*
* 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 { SVGProps } from 'react';
import React from 'react';
export const IconDashboard: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g clipPath="url(#clip0_4044_22930)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3 2C2.44828 2 2 2.44828 2 3V29C2 29.5517 2.44828 30 3 30H29C29.5517 30 30 29.5517 30 29V3C30 2.44828 29.5517 2 29 2H3ZM0 3C0 1.34372 1.34372 0 3 0H29C30.6563 0 32 1.34372 32 3V29C32 30.6563 30.6563 32 29 32H3C1.34372 32 0 30.6563 0 29V3Z"
fill="#535966"
/>
<path fillRule="evenodd" clipRule="evenodd" d="M8 6H5V4H8V6Z" fill="#535966" />
<path fillRule="evenodd" clipRule="evenodd" d="M14 6H11V4H14V6Z" fill="#535966" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M11.3333 30V8H13.2V30H11.3333Z"
fill="#00BFB3"
/>
<path fillRule="evenodd" clipRule="evenodd" d="M30 9.91304H2V8H30V9.91304Z" fill="#00BFB3" />
<path fillRule="evenodd" clipRule="evenodd" d="M5 13H8V15H5V13Z" fill="#00BFB3" />
<path fillRule="evenodd" clipRule="evenodd" d="M5 18H8V20H5V18Z" fill="#00BFB3" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M23.8277 15.1715C22.2663 13.6095 19.7337 13.6095 18.1723 15.1715C16.6095 16.7342 16.6097 19.2668 18.1716 20.8277L18.1723 20.8285C19.7337 22.3905 22.2663 22.3905 23.8277 20.8285L23.8284 20.8277C25.3903 19.2668 25.3905 16.7342 23.8277 15.1715ZM25.2422 13.7576C22.8997 11.4141 19.1003 11.4141 16.7578 13.7576C14.4141 16.1011 14.4141 19.9001 16.7578 22.2424C19.1003 24.5859 22.8997 24.5859 25.2422 22.2424C27.5859 19.9001 27.5859 16.1011 25.2422 13.7576Z"
fill="#535966"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M27.6572 26.0708L24.8282 23.2428L26.2422 21.8284L29.0712 24.6564L27.6572 26.0708Z"
fill="#535966"
/>
</g>
<defs>
<clipPath id="clip0_4044_22930">
<rect width="32" height="32" fill="white" />
</clipPath>
</defs>
</svg>
);
// eslint-disable-next-line import/no-default-export
export default IconDashboard;

View file

@ -0,0 +1,52 @@
/*
* 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 { SVGProps } from 'react';
import React from 'react';
export const IconDataView: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0.470703 11.6253H7.72125V30.5882H0.470703V11.6253ZM2.14391 13.2985V28.915H6.04805V13.2985H2.14391Z"
fill="#535766"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9.95221 0.470581H17.2028V30.5882H9.95221V0.470581ZM11.6254 2.14378V28.915H15.5295V2.14378H11.6254Z"
fill="#535766"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M21.1069 8.27887V15.2505H19.4337V6.60566H26.6843V13.2231H25.0111V8.27887H21.1069Z"
fill="#535766"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M19.4336 22.2222C19.4336 19.1419 21.9307 16.6449 25.0109 16.6449C28.0912 16.6449 30.5883 19.1419 30.5883 22.2222C30.5883 25.3025 28.0912 27.7996 25.0109 27.7996C21.9307 27.7996 19.4336 25.3025 19.4336 22.2222ZM25.0109 18.3181C22.8547 18.3181 21.1068 20.066 21.1068 22.2222C21.1068 24.3784 22.8547 26.1264 25.0109 26.1264C27.1671 26.1264 28.9151 24.3784 28.9151 22.2222C28.9151 20.066 27.1671 18.3181 25.0109 18.3181Z"
fill="#00BFB3"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M30.1504 29.1025L27.5016 26.4537L28.6847 25.2706L31.3335 27.9193L30.1504 29.1025Z"
fill="#00BFB3"
/>
</svg>
);
// eslint-disable-next-line import/no-default-export
export default IconDataView;

View file

@ -0,0 +1,41 @@
/*
* 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 { SVGProps } from 'react';
import React from 'react';
export const IconEcctl: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1 1H24V13.0105H22V3H3V21H10V23H1V1Z"
fill="#535766"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8.5858 9.00001L6.29291 6.70712L7.70712 5.29291L11.4142 9.00001L7.70712 12.7071L6.29291 11.2929L8.5858 9.00001Z"
fill="#00BFB3"
/>
<path fillRule="evenodd" clipRule="evenodd" d="M16 16H10V14H16V16Z" fill="#00BFB3" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M18 20.9988C18 20.9992 18 20.9996 18 21L18.0025 21.9228L17.1099 22.0214C15.361 22.2147 14 23.6991 14 25.5C14 27.433 15.567 29 17.5 29H25.5C27.433 29 29 27.433 29 25.5C29 23.9758 28.0251 22.6767 26.6618 22.1972L25.9576 21.9495L25.9949 21.2039C25.9983 21.1364 26 21.0685 26 21C26 18.7909 24.2091 17 22 17C19.7913 17 18.0007 18.7902 18 20.9988ZM16.0539 20.1923C16.4484 17.2606 18.9602 15 22 15C25.177 15 27.7772 17.4692 27.9864 20.5931C29.7737 21.5005 31 23.356 31 25.5C31 28.5376 28.5376 31 25.5 31H17.5C14.4624 31 12 28.5376 12 25.5C12 22.9626 13.7176 20.8275 16.0539 20.1923Z"
fill="#535766"
/>
</svg>
);
// eslint-disable-next-line import/no-default-export
export default IconEcctl;

View file

@ -0,0 +1,45 @@
/*
* 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 { SVGProps } from 'react';
import React from 'react';
export const IconEndpoint: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g>
<path
id="Fill 1"
fillRule="evenodd"
clipRule="evenodd"
d="M7.74499 2.85648L15.1476 16.0026L14.8713 16.4933L7.7449 29.1484C5.89634 32.4299 1 31.078 1 27.3123V4.69299C1 0.927492 5.89616 -0.424721 7.74499 2.85648ZM3 27.3097C3 29.0366 5.17293 29.6366 6.0023 28.1643L12.8524 16.0001L6.00242 3.83547C5.17299 2.36345 3 2.96358 3 4.69043V27.3097Z"
fill="#535766"
/>
<path
id="Shape"
fillRule="evenodd"
clipRule="evenodd"
d="M10.0122 31L17.6866 17H30.4824L29.5893 18.5093L23.903 28.1177C22.7586 29.9128 20.7657 31 18.6236 31H10.0122ZM22.1992 27.0709L26.975 19H18.8711L13.3893 29H18.6236C20.0841 29 21.4404 28.2602 22.1992 27.0709Z"
fill="#535766"
/>
<path
id="Combined Shape"
fillRule="evenodd"
clipRule="evenodd"
d="M17.1368 14L10.0122 1H18.6234C20.7655 1 22.7585 2.08715 23.9028 3.88232L30.3239 14H27.9552L22.2153 4.9557C21.4402 3.73981 20.0839 3 18.6234 3H13.389L19.4175 14H17.1368Z"
fill="#00BFB3"
/>
</g>
</svg>
);
// eslint-disable-next-line import/no-default-export
export default IconEndpoint;

View file

@ -0,0 +1,32 @@
/*
* 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 { SVGProps } from 'react';
import React from 'react';
export const IconFilebeat: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5 2C4.44828 2 4 2.44828 4 3V29C4 29.5517 4.44828 30 5 30H27C27.5517 30 28 29.5517 28 29V9.41421L20.5858 2H5ZM2 3C2 1.34372 3.34372 0 5 0H21.4142L30 8.58579V29C30 30.6563 28.6563 32 27 32H5C3.34372 32 2 30.6563 2 29V3Z"
fill="#535966"
/>
<path fillRule="evenodd" clipRule="evenodd" d="M20 1H22V8H29V10H20V1Z" fill="#535966" />
<path fillRule="evenodd" clipRule="evenodd" d="M24 20H8V18H24V20Z" fill="#00BFB3" />
<path fillRule="evenodd" clipRule="evenodd" d="M17 15H8V13H17V15Z" fill="#00BFB3" />
<path fillRule="evenodd" clipRule="evenodd" d="M24 25H8V23H24V25Z" fill="#00BFB3" />
</svg>
);
// eslint-disable-next-line import/no-default-export
export default IconFilebeat;

View file

@ -0,0 +1,42 @@
/*
* 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 { SVGProps } from 'react';
import React from 'react';
export const IconFilebeatChart: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5 2C4.44828 2 4 2.44828 4 3V29C4 29.5517 4.44828 30 5 30H27C27.5517 30 28 29.5517 28 29V9.41421L20.5858 2H5ZM2 3C2 1.34372 3.34372 0 5 0H21.4142L30 8.58579V29C30 30.6563 28.6563 32 27 32H5C3.34372 32 2 30.6563 2 29V3Z"
fill="#535966"
/>
<path fillRule="evenodd" clipRule="evenodd" d="M20 1H22V8H29V10H20V1Z" fill="#535966" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M16.5 10L16.5 26H14.5L14.5 10L16.5 10Z"
fill="#00BFB3"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M11.5 16L11.5 26H9.5L9.5 16H11.5Z"
fill="#00BFB3"
/>
<path fillRule="evenodd" clipRule="evenodd" d="M21.5 20V26H19.5V20H21.5Z" fill="#00BFB3" />
</svg>
);
// eslint-disable-next-line import/no-default-export
export default IconFilebeatChart;

View file

@ -0,0 +1,38 @@
/*
* 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 { SVGProps } from 'react';
import React from 'react';
export const IconFleet: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g>
<path
id="Subtract"
fillRule="evenodd"
clipRule="evenodd"
d="M2.99581 18.6363L0 20.4337V26.5661L6 30.1661L11 27.1661L16 30.1661L21 27.1661L26 30.1661L32 26.5661V20.4337L27 17.4337V12.4337L22 9.43374V4.43374L16 0.83374L10 4.43374V7.26581L12 7.29946V5.56612L16 3.16612L20 5.56612V9.43374L18.0041 10.6313L19.0042 12.3636L21 11.1661L25 13.5661V17.4337L21 19.8337L18.7433 18.4797L17.772 20.2293L20 21.5661V25.4337L16 27.8337L12 25.4337V23.7342L10 23.7006V25.4337L6 27.8337L2 25.4337L2 21.5661L3.99595 20.3686L2.99581 18.6363ZM22 25.4337V21.5661L26 19.1661L30 21.5661V25.4337L26 27.8337L22 25.4337Z"
fill="#535766"
/>
<path
id="Polygon 7 (Stroke)"
fillRule="evenodd"
clipRule="evenodd"
d="M11 22.1662L5 18.5662L5 12.4338L11 8.83382L17 12.4338L17 18.5662L11 22.1662ZM15 17.4338L15 13.5662L11 11.1662L7 13.5662L7 17.4338L11 19.8338L15 17.4338Z"
fill="#00BFB3"
/>
</g>
</svg>
);
// eslint-disable-next-line import/no-default-export
export default IconFleet;

View file

@ -0,0 +1,44 @@
/*
* 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 { SVGProps } from 'react';
import React from 'react';
export const IconInfra: React.FC<SVGProps<SVGSVGElement>> = (props) => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g clipPath="url(#clip0_4044_22869)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9 14H32V32H9V14ZM11 16V30H30V16H11Z"
fill="#535966"
/>
<path fillRule="evenodd" clipRule="evenodd" d="M31 24H10V22H31V24Z" fill="#535966" />
<path fillRule="evenodd" clipRule="evenodd" d="M17 20H13V18H17V20Z" fill="#00BFB3" />
<path fillRule="evenodd" clipRule="evenodd" d="M17 28H13V26H17V28Z" fill="#00BFB3" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.62311 7.96014C6.26408 3.46188 10.1221 0 14.8 0C19.4791 0 23.3361 3.46211 23.9778 7.96028C25.9375 8.72369 27.5578 10.1658 28.5476 11.9999H26.1676C25.3118 10.9 24.1262 10.0685 22.7605 9.65388L22.1015 9.45383L22.0534 8.76683C21.7887 4.98508 18.6462 2 14.8 2C10.9547 2 7.8114 4.98516 7.54758 8.7666L7.49962 9.45401L6.84019 9.65397C4.03728 10.5039 2 13.1044 2 16.18C2 19.3164 4.11694 21.9582 7 22.7545V24.812C3.00198 23.9734 0 20.4274 0 16.18C0 12.435 2.33352 9.24079 5.62311 7.96014ZM27.2444 13.9999C27.275 14.0908 27.3038 14.1825 27.3307 14.275L28.2775 13.9999H27.2444Z"
fill="#535966"
/>
</g>
<defs>
<clipPath id="clip0_4044_22869">
<rect width="32" height="32" fill="white" />
</clipPath>
</defs>
</svg>
);
// eslint-disable-next-line import/no-default-export
export default IconInfra;

View file

@ -0,0 +1,64 @@
/*
* 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 { SVGProps } from 'react';
import React from 'react';
export const IconIntuitive: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M13 10C12.4477 10 12 10.4477 12 11V21H10V11C10 9.34315 11.3431 8 13 8C14.6569 8 16 9.34315 16 11V21H14V11C14 10.4477 13.5523 10 13 10Z"
fill="#535966"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M17 16C16.4477 16 16 16.4477 16 17V21H14V17C14 15.3431 15.3431 14 17 14C18.6569 14 20 15.3431 20 17V21H18V17C18 16.4477 17.5523 16 17 16Z"
fill="#535966"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M21 18C20.4477 18 20 18.4477 20 19V21H18V19C18 17.3431 19.3431 16 21 16C22.6569 16 24 17.3431 24 19V21H22V19C22 18.4477 21.5523 18 21 18Z"
fill="#535966"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M25 19C24.4477 19 24 19.4477 24 20V22.3607H22V20C22 18.3431 23.3431 17 25 17C26.6569 17 28 18.3431 28 20V23.8524C28 24.6992 27.7849 25.5321 27.375 26.2731L24.8909 30.7628C24.6813 31.1415 24.5714 31.5672 24.5714 32H22.5714C22.5714 31.2285 22.7674 30.4696 23.1409 29.7946L25.625 25.3048C25.871 24.8603 26 24.3605 26 23.8524V20C26 19.4477 25.5523 19 25 19Z"
fill="#535966"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8.90532 18.7152L10.8096 17.2146L12.0475 18.7854L10.1432 20.2861C9.4213 20.8549 9 21.7233 9 22.6424V24.6827C9 25.3728 9.23793 26.0418 9.67369 26.577L12.1551 29.6243C12.7016 30.2954 13 31.1345 13 32H11C11 31.5946 10.8602 31.2015 10.6042 30.8871L8.12282 27.8398C7.39655 26.9479 7 25.8329 7 24.6827V22.6424C7 21.1106 7.70217 19.6633 8.90532 18.7152Z"
fill="#535966"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12.5 0C6.70101 0 2 4.70101 2 10.5C2 14.218 3.93245 17.4847 6.84774 19.3504C7.17452 18.8632 7.58858 18.4312 8.07723 18.0791L8.321 17.9035C5.74124 16.4442 4 13.6754 4 10.5C4 5.80558 7.80558 2 12.5 2C17.1944 2 21 5.80558 21 10.5C21 11.831 20.6941 13.0905 20.1487 14.2121C20.3579 14.454 20.5344 14.7248 20.6716 15.0178C20.7794 15.006 20.889 15 21 15C21.3209 15 21.6301 15.0504 21.9199 15.1437C22.6114 13.7436 23 12.1672 23 10.5C23 4.70101 18.299 0 12.5 0Z"
fill="#00BFB3"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6 10.5C6 6.91015 8.91015 4 12.5 4C16.0899 4 19 6.91015 19 10.5C19 11.4358 18.8022 12.3255 18.4462 13.1294C18.1452 13.0451 17.8279 13 17.5 13C17.3302 13 17.1633 13.0121 17 13.0354V11C17 10.9185 16.9976 10.8376 16.9928 10.7574C16.9976 10.6722 17 10.5864 17 10.5C17 8.01472 14.9853 6 12.5 6C10.0147 6 8 8.01472 8 10.5C8 11.5716 8.3746 12.5558 9 13.3287V15.9782C7.19584 14.8231 6 12.8012 6 10.5Z"
fill="#00BFB3"
/>
</svg>
);
// eslint-disable-next-line import/no-default-export
export default IconIntuitive;

View 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 type { SVGProps } from 'react';
import React from 'react';
export const IconJobs: React.FC<SVGProps<SVGSVGElement>> = (props) => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g clipPath="url(#clip0_4044_22880)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M25.9995 17C24.8948 17 23.9995 17.8953 23.9995 19C23.9995 20.1047 24.8948 21 25.9995 21C27.1043 21 27.9995 20.1047 27.9995 19C27.9995 17.8953 27.1043 17 25.9995 17ZM21.9995 19C21.9995 16.7907 23.7903 15 25.9995 15C28.2088 15 29.9995 16.7907 29.9995 19C29.9995 21.2093 28.2088 23 25.9995 23C23.7903 23 21.9995 21.2093 21.9995 19Z"
fill="#00BFB3"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M29.9995 27C29.9995 24.7903 28.2093 23 25.9995 23V21C29.3138 21 31.9995 23.6857 31.9995 27V32H29.9995V27Z"
fill="#00BFB3"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M22.4278 25.1954C22.1541 25.7366 21.9995 26.3483 21.9995 27V32H19.9995V27C19.9995 26.0277 20.231 25.1074 20.6432 24.2926L22.4278 25.1954Z"
fill="#00BFB3"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M19.9504 19.0408C20.8348 20.2317 22.2483 21 23.8425 21L25.9995 21V23L23.8425 23C21.5888 23 19.5903 21.9103 18.3447 20.2332L19.9504 19.0408Z"
fill="#00BFB3"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0 0H22V11H20V2H2V20H14V22H0V0Z"
fill="#535966"
/>
<path fillRule="evenodd" clipRule="evenodd" d="M15 8H5V6H15V8Z" fill="#535966" />
<path fillRule="evenodd" clipRule="evenodd" d="M12 12H5V10H12V12Z" fill="#535966" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M15.8575 12.4855L18.8575 17.4855L17.1425 18.5145L14.1425 13.5145L15.8575 12.4855Z"
fill="#535966"
/>
</g>
<defs>
<clipPath id="clip0_4044_22880">
<rect width="32" height="32" fill="white" />
</clipPath>
</defs>
</svg>
);
// eslint-disable-next-line import/no-default-export
export default IconJobs;

View file

@ -0,0 +1,34 @@
/*
* 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 { SVGProps } from 'react';
import React from 'react';
export const IconKeyword: React.FC<SVGProps<SVGSVGElement>> = (props) => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M16 1C7.71573 1 1 7.71573 1 16C1 24.2843 7.71573 31 16 31C24.2843 31 31 24.2843 31 16C31 7.71573 24.2843 1 16 1ZM15 3.03789C8.61759 3.52341 3.52341 8.61759 3.03789 15H6V17H3.03789C3.52341 23.3824 8.61759 28.4766 15 28.9621V26H17V28.9621C23.3824 28.4766 28.4766 23.3824 28.9621 17H26V15H28.9621C28.4766 8.61759 23.3824 3.52341 17 3.03789V6H15V3.03789Z"
fill="#535966"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M15.3408 9H16.6596L22.6596 23H20.4837L19.1979 20L12.8025 20L11.5168 23H9.34082L15.3408 9ZM16.0002 12.5386L18.3408 18L13.6596 18L16.0002 12.5386Z"
fill="#00BFB3"
/>
</svg>
);
// eslint-disable-next-line import/no-default-export
export default IconKeyword;

View file

@ -0,0 +1,54 @@
/*
* 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 { SVGProps } from 'react';
import React from 'react';
export const IconLens: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g clipPath="url(#clip0_2634_421078)">
<g id="Group">
<path id="Path 37" d="M22 13V5H20V15L22 13Z" fill="#00BFB3" />
<path
id="Path"
d="M17 0C25.2843 0 32 6.71573 32 15C32 23.2843 25.2843 30 17 30C8.71573 30 2 23.2843 2 15C2 13.9505 2.10777 12.9263 2.31286 11.9376L4.21918 12.6102C4.07525 13.3849 4 14.1836 4 15C4 22.1797 9.8203 28 17 28C24.1797 28 30 22.1797 30 15C30 7.8203 24.1797 2 17 2C15.9978 2 15.0222 2.1134 14.0851 2.32807L13.3344 0.451093C14.5076 0.156466 15.7355 0 17 0Z"
fill="#535766"
/>
<path
id="Path_2"
d="M9.62122 1.93732L10.3719 3.81422C8.34439 5.01818 6.67223 6.75769 5.55036 8.83777L3.64404 8.16511C4.98472 5.55058 7.07056 3.38121 9.62122 1.93732Z"
fill="#535766"
/>
<path
id="Path 36"
d="M1.70718 31.7071L7.70718 25.7071L6.29297 24.2929L0.292969 30.2929L1.70718 31.7071Z"
fill="#535766"
/>
<path id="Path 37_2" d="M12 16V11H10V18L12 16Z" fill="#00BFB3" />
<path id="Path 37_3" d="M17 15V8H15V13L17 15Z" fill="#00BFB3" />
<path
id="Path 36_2"
d="M10.7072 23.2071L15.0488 18.8655L18.7988 22.1155L25.2072 15.7071L23.793 14.2929L18.7014 19.3845L14.9514 16.1345L9.29297 21.7929L10.7072 23.2071Z"
fill="#00BFB3"
/>
</g>
</g>
<defs>
<clipPath id="clip0_2634_421078">
<rect width="32" height="32" fill="white" />
</clipPath>
</defs>
</svg>
);
// eslint-disable-next-line import/no-default-export
export default IconLens;

View file

@ -0,0 +1,61 @@
/*
* 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 { SVGProps } from 'react';
import React from 'react';
export const IconManager: React.FC<SVGProps<SVGSVGElement>> = (props) => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g clipPath="url(#clip0_4044_22953)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0 24H8V32H0V24ZM2 26V30H6V26H2Z"
fill="#535966"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M24 24H32V32H24V24ZM26 26V30H30V26H26Z"
fill="#535966"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12 24H20V32H12V24ZM14 26V30H18V26H14Z"
fill="#535966"
/>
<path fillRule="evenodd" clipRule="evenodd" d="M4 17H28V22H26V19H6V22H4V17Z" fill="#00BFB3" />
<path fillRule="evenodd" clipRule="evenodd" d="M17 14V22H15V14H17Z" fill="#00BFB3" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 12.1698C10 8.79382 12.6553 6 16 6C19.3447 6 22 8.79382 22 12.1698V13H20V12.1698C20 9.83501 18.1778 8 16 8C13.8222 8 12 9.83501 12 12.1698V13H10V12.1698Z"
fill="#535966"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M16 2C14.8955 2 14 2.8955 14 4C14 5.1045 14.8955 6 16 6C17.1045 6 18 5.1045 18 4C18 2.8955 17.1045 2 16 2ZM12 4C12 1.79093 13.7909 0 16 0C18.2091 0 20 1.79093 20 4C20 6.20907 18.2091 8 16 8C13.7909 8 12 6.20907 12 4Z"
fill="#535966"
/>
</g>
<defs>
<clipPath id="clip0_4044_22953">
<rect width="32" height="32" fill="white" />
</clipPath>
</defs>
</svg>
);
// eslint-disable-next-line import/no-default-export
export default IconManager;

View file

@ -0,0 +1,49 @@
/*
* 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 { SVGProps } from 'react';
import React from 'react';
export const IconMarketing: React.FC<SVGProps<SVGSVGElement>> = (props) => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g clipPath="url(#clip0_4246_67063)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M26 0.408508V28.581L10.014 21H6.44576C2.88586 21 0 18.1141 0 14.5542C0 10.9943 2.88586 8.10848 6.44576 8.10848H10.0109L26 0.408508ZM24 3.59149L10.4674 10.1085H6.44576C3.99043 10.1085 2 12.0989 2 14.5542C2 17.0096 3.99043 19 6.44576 19H10.4642L24 25.419V3.59149Z"
fill="#535966"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8.3214 30L5.9747 19.7763L4.02539 20.2237L6.72846 32H15.3155L14.0842 27.5653L12.2335 26.5106L10.8668 19.8005L8.90704 20.1996L10.4548 27.799L12.3777 28.8948L12.6845 30H8.3214Z"
fill="#535966"
/>
<path fillRule="evenodd" clipRule="evenodd" d="M8 17V12H10V17H8Z" fill="#00BFB3" />
<path fillRule="evenodd" clipRule="evenodd" d="M18 21V8H20V21H18Z" fill="#00BFB3" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M28 19.8922C30.3504 19.3757 32 17.0722 32 14.5C32 11.9278 30.3504 9.62432 28 9.10779V11.2044C29.1207 11.6726 30 12.9063 30 14.5C30 16.0937 29.1207 17.3274 28 17.7956V19.8922Z"
fill="#00BFB3"
/>
</g>
<defs>
<clipPath id="clip0_4246_67063">
<rect width="32" height="32" fill="white" />
</clipPath>
</defs>
</svg>
);
// eslint-disable-next-line import/no-default-export
export default IconMarketing;

View file

@ -0,0 +1,64 @@
/*
* 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 { SVGProps } from 'react';
import React from 'react';
export const IconOsquery: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g id="Group">
<path
id="Vector"
d="M31.9071 0.0319824V8.00098L23.9551 15.952V7.96698L31.9071 0.0319824Z"
fill="#00BFB3"
/>
<path
id="Vector_2"
d="M16.0029 0.0319824V8.00098L23.9549 15.952V7.96698L16.0029 0.0319824Z"
fill="#333333"
/>
<path
id="Vector_3"
d="M31.9229 31.855H23.9549L16.0029 23.904H23.9879L31.9229 31.855Z"
fill="#00BFB3"
/>
<path
id="Vector_4"
d="M31.9229 15.952H23.9549L16.0029 23.904H23.9879L31.9229 15.952Z"
fill="#333333"
/>
<path
id="Vector_5"
d="M0.100098 31.872V23.904L8.0521 15.952V23.937L0.100098 31.872Z"
fill="#00BFB3"
/>
<path
id="Vector_6"
d="M16.004 31.872V23.904L8.052 15.952V23.937L16.004 31.872Z"
fill="#333333"
/>
<path
id="Vector_7"
d="M0.0839844 0.0479736H8.05198L16.004 7.99997H8.01898L0.0839844 0.0479736Z"
fill="#00BFB3"
/>
<path
id="Vector_8"
d="M0.0839844 15.952H8.05198L16.004 8H8.01898L0.0839844 15.952Z"
fill="#333333"
/>
</g>
</svg>
);
// eslint-disable-next-line import/no-default-export
export default IconOsquery;

View file

@ -0,0 +1,46 @@
/*
* 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 { SVGProps } from 'react';
import React from 'react';
export const IconRapidBarGraph: React.FC<SVGProps<SVGSVGElement>> = (props) => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.5 2.5V26.5C3.5 27.6044 4.3953 28.5 5.5 28.5H29.5V30H5.5C3.5667 30 2 28.4326 2 26.5V2.5H3.5Z"
fill="#535966"
/>
<path fillRule="evenodd" clipRule="evenodd" d="M8 25.5V18.5H9.5V25.5H8Z" fill="#535966" />
<path fillRule="evenodd" clipRule="evenodd" d="M14 25.5V12H15.5V25.5H14Z" fill="#535966" />
<path fillRule="evenodd" clipRule="evenodd" d="M20 25.5V16.5H21.5V25.5H20Z" fill="#535966" />
<path fillRule="evenodd" clipRule="evenodd" d="M26 25.5V19H27.5V25.5H26Z" fill="#535966" />
<path fillRule="evenodd" clipRule="evenodd" d="M8 16.4974V12H9.5V16.4974H8Z" fill="#00BFB3" />
<path fillRule="evenodd" clipRule="evenodd" d="M14 9.5V5H15.5V9.5H14Z" fill="#00BFB3" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M20 13.9827V8.5H21.5V13.9827H20Z"
fill="#00BFB3"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M26 16.4827V13H27.5V16.4827H26Z"
fill="#00BFB3"
/>
</svg>
);
// eslint-disable-next-line import/no-default-export
export default IconRapidBarGraph;

View file

@ -0,0 +1,55 @@
/*
* 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 { SVGProps } from 'react';
import React from 'react';
export const IconReplication: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g clipPath="url(#clip0_4044_23007)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0 0H14V10.0035L8.70081 16H0V0ZM2 2V14H7.79919L12 9.24646V2H2Z"
fill="#535966"
/>
<path fillRule="evenodd" clipRule="evenodd" d="M6 8H12V10H8V14H6V8Z" fill="#535966" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M18 16H32V26.0035L26.7008 32H18V16ZM20 18V30H25.7992L30 25.2465V18H20Z"
fill="#535966"
/>
<path fillRule="evenodd" clipRule="evenodd" d="M24 24H30V26H26V30H24V24Z" fill="#535966" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M14 29C7.92465 29 3 24.0742 3 18H5C5 22.9698 9.02935 27 14 27H16V29H14Z"
fill="#00BFB3"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M18 5H16V3H18C24.0753 3 29 7.92578 29 14H27C27 9.03022 22.9706 5 18 5Z"
fill="#00BFB3"
/>
</g>
<defs>
<clipPath id="clip0_4044_23007">
<rect width="32" height="32" fill="white" />
</clipPath>
</defs>
</svg>
);
// eslint-disable-next-line import/no-default-export
export default IconReplication;

View file

@ -0,0 +1,41 @@
/*
* 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 { SVGProps } from 'react';
import React from 'react';
export const IconSettings: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g clipPath="url(#clip0_4044_22923)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M26.8384 20.479C27.1552 19.7077 27.9008 19.2 28.736 19.2H32V12.8H28.736C27.9008 12.8 27.1552 12.2923 26.8384 11.5211C26.8352 11.5125 26.832 11.504 26.8277 11.4955C26.5109 10.7264 26.6795 9.84533 27.2683 9.2576L29.5765 6.94933L25.0507 2.42347L22.7424 4.73173C22.1547 5.31947 21.2736 5.48907 20.5045 5.17227C20.5008 5.17088 20.4974 5.1693 20.4938 5.16769C20.4891 5.16558 20.4844 5.16342 20.4789 5.1616C19.7077 4.8448 19.2 4.0992 19.2 3.26507V0H12.8V3.26507C12.8 4.0992 12.2923 4.8448 11.5211 5.1616C11.5156 5.16342 11.5109 5.16558 11.5062 5.16769C11.5027 5.1693 11.4992 5.17088 11.4955 5.17227C10.7264 5.48907 9.84533 5.31947 9.2576 4.73173L6.94933 2.42347L2.42347 6.94933L4.73173 9.2576C5.32053 9.84533 5.48907 10.7264 5.17227 11.4955C5.168 11.504 5.1648 11.5125 5.1616 11.5211C4.8448 12.2923 4.0992 12.8 3.264 12.8H0V19.2H3.264C4.0992 19.2 4.8448 19.7077 5.1616 20.4789C5.1648 20.4875 5.168 20.496 5.17227 20.5045C5.48907 21.2736 5.32053 22.1547 4.73173 22.7424L2.42347 25.0507L6.94933 29.5765L9.2576 27.2683C9.84533 26.6805 10.7264 26.5109 11.4955 26.8277C11.4992 26.8291 11.5027 26.8307 11.5062 26.8323C11.5109 26.8344 11.5156 26.8366 11.5211 26.8384C12.2923 27.1552 12.8 27.9008 12.8 28.7349V32H19.2V28.7349C19.2 27.9008 19.7077 27.1552 20.4789 26.8384C20.4844 26.8366 20.4891 26.8344 20.4938 26.8323C20.4974 26.8307 20.5008 26.8291 20.5045 26.8277C21.2736 26.5109 22.1547 26.6805 22.7424 27.2683L25.0507 29.5765L29.5765 25.0507L27.2683 22.7424C26.6795 22.1547 26.5109 21.2736 26.8277 20.5045C26.832 20.496 26.8352 20.4875 26.8384 20.479ZM25.7611 24.2523C24.5504 23.0437 24.2165 21.2425 24.8552 19.692L24.8848 19.6201L24.8877 19.6144C25.5407 18.0852 27.0422 17.0667 28.736 17.0667H29.8667V14.9333H28.736C27.0422 14.9333 25.5407 13.9148 24.8877 12.3856L24.8848 12.3799L24.8552 12.308C24.2166 10.7578 24.5502 8.95696 25.7605 7.74837C25.7607 7.74816 25.7603 7.74859 25.7605 7.74837L26.5595 6.94933L25.0507 5.44046L24.2509 6.24023C23.0557 7.43543 21.2788 7.77771 19.7367 7.16293L19.7355 7.1625L19.7277 7.15931C19.7205 7.15643 19.7133 7.15353 19.7062 7.15061C19.6901 7.14415 19.6759 7.1382 19.6639 7.13309C18.1078 6.49182 17.0667 4.9766 17.0667 3.26507V2.13333H14.9333V3.26507C14.9333 4.97661 13.8922 6.49182 12.3361 7.13309C12.3242 7.1382 12.31 7.14412 12.2939 7.15057C12.2867 7.15352 12.2795 7.15645 12.2722 7.15936L12.2645 7.1625L12.2633 7.16292C10.7212 7.77771 8.94431 7.43544 7.74911 6.24023L6.94933 5.44046L5.44046 6.94933L6.23886 7.74774C6.23921 7.74809 6.23956 7.74843 6.2399 7.74878C6.24001 7.74889 6.23979 7.74867 6.2399 7.74878C7.44957 8.95736 7.78325 10.7581 7.1448 12.308L7.11517 12.3799L7.11234 12.3856C6.45935 13.9148 4.95778 14.9333 3.264 14.9333H2.13333V17.0667H3.264C4.95778 17.0667 6.45935 18.0852 7.11235 19.6144L7.11517 19.6201L7.1448 19.692C7.78325 21.2419 7.4499 23.0423 6.24023 24.2509C6.24012 24.251 6.24034 24.2508 6.24023 24.2509C6.23988 24.2512 6.23921 24.2519 6.23886 24.2523L5.44046 25.0507L6.94933 26.5595L7.74911 25.7598C8.94432 24.5646 10.7212 24.2223 12.2633 24.8371L12.2645 24.8375L12.2721 24.8406C12.2795 24.8436 12.2868 24.8465 12.2942 24.8495C12.3101 24.8559 12.3243 24.8618 12.3362 24.8669C13.8922 25.5082 14.9333 27.0234 14.9333 28.7349V29.8667H17.0667V28.7349C17.0667 27.0234 18.1078 25.5082 19.6638 24.8669C19.6758 24.8618 19.6899 24.8559 19.7058 24.8495C19.7131 24.8466 19.7204 24.8436 19.7277 24.8407L19.7355 24.8375L19.7367 24.8371C21.2788 24.2223 23.0557 24.5646 24.2509 25.7598L25.0507 26.5595L26.5595 25.0507L25.7611 24.2523ZM7.1591 19.7299L7.15841 19.728ZM7.1591 12.2701C7.16029 12.267 7.15989 12.2681 7.15836 12.2721L7.1591 12.2701ZM24.8409 12.2701L24.8417 12.2722ZM24.8409 19.7299C24.8397 19.733 24.8401 19.7319 24.8416 19.7279L24.8409 19.7299Z"
fill="#535966"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M16 13C14.3433 13 13 14.3433 13 16C13 17.6567 14.3433 19 16 19C17.6567 19 19 17.6567 19 16C19 14.3433 17.6567 13 16 13ZM11 16C11 13.2387 13.2387 11 16 11C18.7613 11 21 13.2387 21 16C21 18.7613 18.7613 21 16 21C13.2387 21 11 18.7613 11 16Z"
fill="#00BFB3"
/>
</g>
<defs>
<clipPath id="clip0_4044_22923">
<rect width="32" height="32" fill="white" />
</clipPath>
</defs>
</svg>
);
// eslint-disable-next-line import/no-default-export
export default IconSettings;

View file

@ -0,0 +1,34 @@
/*
* 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 { SVGProps } from 'react';
import React from 'react';
export const IconTimeline: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4 15C4 14.7348 4.10536 14.4804 4.29289 14.2929C4.48043 14.1054 4.73478 14 5 14C5.26522 14 5.51957 14.1054 5.70711 14.2929C5.89464 14.4804 6 14.7348 6 15C6 15.2652 5.89464 15.5196 5.70711 15.7071C5.51957 15.8946 5.26522 16 5 16C4.73478 16 4.48043 15.8946 4.29289 15.7071C4.10536 15.5196 4 15.2652 4 15ZM5 18C4.37935 18.0003 3.77387 17.8081 3.26702 17.4499C2.76016 17.0917 2.37688 16.5852 2.17 16H1C0.734784 16 0.48043 15.8946 0.292893 15.7071C0.105357 15.5196 0 15.2652 0 15C0 14.7348 0.105357 14.4804 0.292893 14.2929C0.48043 14.1054 0.734784 14 1 14H2.17C2.37614 13.414 2.7591 12.9065 3.26602 12.5474C3.77294 12.1884 4.37881 11.9955 5 11.9955C5.62119 11.9955 6.22707 12.1884 6.73398 12.5474C7.2409 12.9065 7.62386 13.414 7.83 14H12.17C12.3761 13.414 12.7591 12.9065 13.266 12.5474C13.7729 12.1884 14.3788 11.9955 15 11.9955C15.6212 11.9955 16.2271 12.1884 16.734 12.5474C17.2409 12.9065 17.6239 13.414 17.83 14H22.17C22.3761 13.414 22.7591 12.9065 23.266 12.5474C23.7729 12.1884 24.3788 11.9955 25 11.9955C25.6212 11.9955 26.2271 12.1884 26.734 12.5474C27.2409 12.9065 27.6239 13.414 27.83 14H29C29.2652 14 29.5196 14.1054 29.7071 14.2929C29.8946 14.4804 30 14.7348 30 15C30 15.2652 29.8946 15.5196 29.7071 15.7071C29.5196 15.8946 29.2652 16 29 16H27.83C27.6239 16.586 27.2409 17.0935 26.734 17.4526C26.2271 17.8116 25.6212 18.0045 25 18.0045C24.3788 18.0045 23.7729 17.8116 23.266 17.4526C22.7591 17.0935 22.3761 16.586 22.17 16H17.83C17.6239 16.586 17.2409 17.0935 16.734 17.4526C16.2271 17.8116 15.6212 18.0045 15 18.0045C14.3788 18.0045 13.7729 17.8116 13.266 17.4526C12.7591 17.0935 12.3761 16.586 12.17 16H7.83C7.62312 16.5852 7.23984 17.0917 6.73298 17.4499C6.22613 17.8081 5.62065 18.0003 5 18ZM26 15C26 14.7348 25.8946 14.4804 25.7071 14.2929C25.5196 14.1054 25.2652 14 25 14C24.7348 14 24.4804 14.1054 24.2929 14.2929C24.1054 14.4804 24 14.7348 24 15C24 15.2652 24.1054 15.5196 24.2929 15.7071C24.4804 15.8946 24.7348 16 25 16C25.2652 16 25.5196 15.8946 25.7071 15.7071C25.8946 15.5196 26 15.2652 26 15ZM16 15C16 14.7348 15.8946 14.4804 15.7071 14.2929C15.5196 14.1054 15.2652 14 15 14C14.7348 14 14.4804 14.1054 14.2929 14.2929C14.1054 14.4804 14 14.7348 14 15C14 15.2652 14.1054 15.5196 14.2929 15.7071C14.4804 15.8946 14.7348 16 15 16C15.2652 16 15.5196 15.8946 15.7071 15.7071C15.8946 15.5196 16 15.2652 16 15Z"
fill="#343741"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M14 9C14 9.26522 14.1054 9.51957 14.2929 9.70711C14.4804 9.89464 14.7348 10 15 10C15.2652 10 15.5196 9.89464 15.7071 9.70711C15.8946 9.51957 16 9.26522 16 9V8H18C18.5304 8 19.0391 7.78929 19.4142 7.41421C19.7893 7.03914 20 6.53043 20 6V2C20 1.46957 19.7893 0.960859 19.4142 0.585786C19.0391 0.210714 18.5304 0 18 0L12 0C11.4696 0 10.9609 0.210714 10.5858 0.585786C10.2107 0.960859 10 1.46957 10 2V6C10 6.53043 10.2107 7.03914 10.5858 7.41421C10.9609 7.78929 11.4696 8 12 8H14V9ZM18 2H12V6H18V2ZM5 20C4.73478 20 4.48043 20.1054 4.29289 20.2929C4.10536 20.4804 4 20.7348 4 21V22H2C1.46957 22 0.96086 22.2107 0.585787 22.5858C0.210714 22.9609 0 23.4696 0 24V28C0 28.5304 0.210714 29.0391 0.585787 29.4142C0.96086 29.7893 1.46957 30 2 30H8C8.53043 30 9.03914 29.7893 9.41421 29.4142C9.78929 29.0391 10 28.5304 10 28V24C10 23.4696 9.78929 22.9609 9.41421 22.5858C9.03914 22.2107 8.53043 22 8 22H6V21C6 20.7348 5.89464 20.4804 5.70711 20.2929C5.51957 20.1054 5.26522 20 5 20ZM8 28V24H2V28H8ZM24 21C24 20.7348 24.1054 20.4804 24.2929 20.2929C24.4804 20.1054 24.7348 20 25 20C25.2652 20 25.5196 20.1054 25.7071 20.2929C25.8946 20.4804 26 20.7348 26 21V22H28C28.5304 22 29.0391 22.2107 29.4142 22.5858C29.7893 22.9609 30 23.4696 30 24V28C30 28.5304 29.7893 29.0391 29.4142 29.4142C29.0391 29.7893 28.5304 30 28 30H22C21.4696 30 20.9609 29.7893 20.5858 29.4142C20.2107 29.0391 20 28.5304 20 28V24C20 23.4696 20.2107 22.9609 20.5858 22.5858C20.9609 22.2107 21.4696 22 22 22H24V21ZM28 26V28H22V24H28V26Z"
fill="#00BFB3"
/>
</svg>
);
// eslint-disable-next-line import/no-default-export
export default IconTimeline;

View file

@ -0,0 +1,39 @@
/*
* 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 { SVGProps } from 'react';
import React from 'react';
export const IconVisualization: React.FC<SVGProps<SVGSVGElement>> = ({ ...props }) => (
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
id="Path"
fillRule="evenodd"
clipRule="evenodd"
d="M30 30H4C1.79086 30 0 28.2091 0 26V0H2V26C2 27.1046 2.89543 28 4 28H30V30Z"
fill="#535766"
/>
<rect id="Rectangle" x="6" y="19" width="2" height="7" fill="#535766" />
<rect id="Rectangle_2" x="16" y="11" width="2" height="15" fill="#535766" />
<rect id="Rectangle_3" x="26" y="16" width="2" height="10" fill="#535766" />
<path
id="Shape"
fillRule="evenodd"
clipRule="evenodd"
d="M24.92 5.84C25.4786 5.30157 26.2241 5.0005 27 5C28.6569 5 30 6.34315 30 8C30 9.65685 28.6569 11 27 11C25.3431 11 24 9.65685 24 8C24.0165 7.87797 24.0433 7.75755 24.08 7.64L19.08 5.16C18.5214 5.69843 17.7759 5.9995 17 6C16.4388 5.99095 15.8914 5.82465 15.42 5.52L9.82 10C9.93778 10.3203 9.9987 10.6587 10 11C10 12.6569 8.65685 14 7 14C5.34315 14 4 12.6569 4 11C4 9.34315 5.34315 8 7 8C7.55925 8.00307 8.1065 8.16239 8.58 8.46L14.18 4C14.0622 3.67969 14.0013 3.34127 14 3C14 1.34315 15.3431 0 17 0C18.6569 0 20 1.34315 20 3C20.0098 3.1198 20.0098 3.2402 20 3.36L24.92 5.84ZM6 11C6 11.5523 6.44772 12 7 12C7.55228 12 8 11.5523 8 11C8 10.4477 7.55228 10 7 10C6.44772 10 6 10.4477 6 11ZM17 4C16.4477 4 16 3.55228 16 3C16 2.44772 16.4477 2 17 2C17.5523 2 18 2.44772 18 3C18 3.26522 17.8946 3.51957 17.7071 3.70711C17.5196 3.89464 17.2652 4 17 4ZM26 8C26 8.55229 26.4477 9 27 9C27.5523 9 28 8.55229 28 8C28 7.44772 27.5523 7 27 7C26.4477 7 26 7.44772 26 8Z"
fill="#00BFB3"
/>
</svg>
);
// eslint-disable-next-line import/no-default-export
export default IconVisualization;

View file

@ -5,19 +5,19 @@
* 2.0.
*/
import { SecurityPageName } from '@kbn/security-solution-navigation';
import { SERVER_APP_ID } from '@kbn/security-solution-plugin/common';
import type { LinkItem } from '@kbn/security-solution-plugin/public';
import { ExternalPageName, SecurityPagePath } from '../constants';
import type { ProjectNavigationLink } from '../types';
import { IconOsqueryLazy, IconTimelineLazy } from '../../../common/lazy_icons';
import { ExternalPageName, SecurityPageName } from '@kbn/security-solution-navigation';
import { INVESTIGATIONS_PATH } from '../../../../../common/constants';
import { SERVER_APP_ID } from '../../../../../common';
import type { LinkItem } from '../../../../common/links/types';
import type { SolutionNavLink } from '../../../../common/links';
import { IconOsqueryLazy, IconTimelineLazy } from './lazy_icons';
import * as i18n from './investigations_translations';
// appLinks configures the Security Solution pages links
const investigationsAppLink: LinkItem = {
id: SecurityPageName.investigations,
title: i18n.INVESTIGATIONS_TITLE,
path: SecurityPagePath[SecurityPageName.investigations],
path: INVESTIGATIONS_PATH,
capabilities: [`${SERVER_APP_ID}.show`],
hideTimeline: true,
skipUrlState: true,
@ -34,7 +34,7 @@ export const createInvestigationsLinkFromTimeline = (timelineLink: LinkItem): Li
};
// navLinks define the navigation links for the Security Solution pages and External pages as well
export const investigationsNavLinks: ProjectNavigationLink[] = [
export const investigationsNavLinks: SolutionNavLink[] = [
{
id: ExternalPageName.osquery,
title: i18n.OSQUERY_TITLE,

View file

@ -8,27 +8,27 @@
import { i18n } from '@kbn/i18n';
export const INVESTIGATIONS_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.investigations.title',
'xpack.securitySolution.navLinks.investigations.title',
{
defaultMessage: 'Investigations',
}
);
export const TIMELINE_DESCRIPTION = i18n.translate(
'xpack.securitySolutionServerless.navLinks.investigations.timeline.title',
'xpack.securitySolution.navLinks.investigations.timeline.title',
{
defaultMessage: 'Central place for timelines and timeline templates',
}
);
export const OSQUERY_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.investigations.osquery.title',
'xpack.securitySolution.navLinks.investigations.osquery.title',
{
defaultMessage: 'Osquery',
}
);
export const OSQUERY_DESCRIPTION = i18n.translate(
'xpack.securitySolutionServerless.navLinks.investigations.osquery.description',
'xpack.securitySolution.navLinks.investigations.osquery.description',
{
defaultMessage: 'Deploy Osquery with Elastic Agent, then run and schedule queries in Kibana',
}

View file

@ -0,0 +1,49 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { Suspense } from 'react';
import { EuiLoadingSpinner } from '@elastic/eui';
const withSuspenseIcon = <T extends object = {}>(Component: React.ComponentType<T>): React.FC<T> =>
function WithSuspenseIcon(props) {
return (
<Suspense fallback={<EuiLoadingSpinner size="s" />}>
<Component {...props} />
</Suspense>
);
};
export const IconLensLazy = withSuspenseIcon(React.lazy(() => import('./icons/lens')));
export const IconEndpointLazy = withSuspenseIcon(React.lazy(() => import('./icons/endpoint')));
export const IconFleetLazy = withSuspenseIcon(React.lazy(() => import('./icons/fleet')));
export const IconEcctlLazy = withSuspenseIcon(React.lazy(() => import('./icons/ecctl')));
export const IconTimelineLazy = withSuspenseIcon(React.lazy(() => import('./icons/timeline')));
export const IconOsqueryLazy = withSuspenseIcon(React.lazy(() => import('./icons/osquery')));
export const IconVisualizationLazy = withSuspenseIcon(
React.lazy(() => import('./icons/visualization'))
);
export const IconMarketingLazy = withSuspenseIcon(React.lazy(() => import('./icons/marketing')));
export const IconInfraLazy = withSuspenseIcon(React.lazy(() => import('./icons/infra')));
export const IconKeywordLazy = withSuspenseIcon(React.lazy(() => import('./icons/keyword')));
export const IconJobsLazy = withSuspenseIcon(React.lazy(() => import('./icons/jobs')));
export const IconSettingsLazy = withSuspenseIcon(React.lazy(() => import('./icons/settings')));
export const IconDashboardLazy = withSuspenseIcon(React.lazy(() => import('./icons/dashboard')));
export const IconChartArrowLazy = withSuspenseIcon(React.lazy(() => import('./icons/chart_arrow')));
export const IconManagerLazy = withSuspenseIcon(React.lazy(() => import('./icons/manager')));
export const IconFilebeatLazy = withSuspenseIcon(React.lazy(() => import('./icons/filebeat')));
export const IconDataViewLazy = withSuspenseIcon(React.lazy(() => import('./icons/data_view')));
export const IconReplicationLazy = withSuspenseIcon(
React.lazy(() => import('./icons/replication'))
);
export const IconIntuitiveLazy = withSuspenseIcon(React.lazy(() => import('./icons/intuitive')));
export const IconRapidBarGraphLazy = withSuspenseIcon(
React.lazy(() => import('./icons/rapid_bar_graph'))
);
export const IconFilebeatChartLazy = withSuspenseIcon(
React.lazy(() => import('./icons/filebeat_chart'))
);

View file

@ -5,12 +5,15 @@
* 2.0.
*/
import { SecurityPageName, LinkCategoryType } from '@kbn/security-solution-navigation';
import { SERVER_APP_ID } from '@kbn/security-solution-plugin/common';
import type { LinkItem } from '@kbn/security-solution-plugin/public';
import { ExternalPageName, SecurityPagePath } from '../constants';
import type { ProjectLinkCategory, ProjectNavigationLink } from '../types';
import * as i18n from './ml_translations';
import {
SecurityPageName,
ExternalPageName,
LinkCategoryType,
} from '@kbn/security-solution-navigation';
import { MACHINE_LEARNING_PATH } from '../../../../../common/constants';
import type { LinkItem } from '../../../../common/links/types';
import { SERVER_APP_ID } from '../../../../../common';
import type { SolutionLinkCategory, SolutionNavLink } from '../../../../common/links';
import {
IconLensLazy,
IconMarketingLazy,
@ -28,13 +31,14 @@ import {
IconDataViewLazy,
IconIntuitiveLazy,
IconRapidBarGraphLazy,
} from '../../../common/lazy_icons';
} from './lazy_icons';
import * as i18n from './ml_translations';
// appLinks configures the Security Solution pages links
export const mlAppLink: LinkItem = {
id: SecurityPageName.mlLanding,
title: i18n.ML_TITLE,
path: SecurityPagePath[SecurityPageName.mlLanding],
path: MACHINE_LEARNING_PATH,
capabilities: [`${SERVER_APP_ID}.show`],
globalSearchKeywords: [i18n.ML_KEYWORD],
hideTimeline: true,
@ -42,7 +46,7 @@ export const mlAppLink: LinkItem = {
links: [], // no security internal links
};
export const mlNavCategories: ProjectLinkCategory[] = [
export const mlNavCategories: SolutionLinkCategory[] = [
{
type: LinkCategoryType.separator,
linkIds: [
@ -96,7 +100,7 @@ export const mlNavCategories: ProjectLinkCategory[] = [
];
// navLinks define the navigation links for the Security Solution pages and External pages as well
export const mlNavLinks: ProjectNavigationLink[] = [
export const mlNavLinks: SolutionNavLink[] = [
{
id: ExternalPageName.mlOverview,
title: i18n.OVERVIEW_TITLE,

View file

@ -7,256 +7,238 @@
import { i18n } from '@kbn/i18n';
export const ML_TITLE = i18n.translate('xpack.securitySolutionServerless.appLinks.ml.title', {
export const ML_TITLE = i18n.translate('xpack.securitySolution.appLinks.ml.title', {
defaultMessage: 'Machine learning',
});
export const ML_KEYWORD = i18n.translate('xpack.securitySolutionServerless.appLinks.ml.keyword', {
export const ML_KEYWORD = i18n.translate('xpack.securitySolution.appLinks.ml.keyword', {
defaultMessage: 'Machine learning',
});
export const ANOMALY_DETECTION_CATEGORY = i18n.translate(
'xpack.securitySolutionServerless.navCategories.ml.anomalyDetection.title',
'xpack.securitySolution.navCategories.ml.anomalyDetection.title',
{
defaultMessage: 'Anomaly detection',
}
);
export const DATA_FRAME_ANALYTICS_CATEGORY = i18n.translate(
'xpack.securitySolutionServerless.navCategories.ml.dataFrameAnalyticstitle',
'xpack.securitySolution.navCategories.ml.dataFrameAnalyticstitle',
{
defaultMessage: 'Data frame analytics',
}
);
export const MODEL_MANAGEMENT_CATEGORY = i18n.translate(
'xpack.securitySolutionServerless.navCategories.ml.modelManagement.title',
'xpack.securitySolution.navCategories.ml.modelManagement.title',
{
defaultMessage: 'Model management',
}
);
export const DATA_VISUALIZER_CATEGORY = i18n.translate(
'xpack.securitySolutionServerless.navCategories.ml.dataVisualizer.title',
'xpack.securitySolution.navCategories.ml.dataVisualizer.title',
{
defaultMessage: 'Data visualizer',
}
);
export const AIOPS_LABS_CATEGORY = i18n.translate(
'xpack.securitySolutionServerless.navCategories.ml.aiopsLabs.title',
'xpack.securitySolution.navCategories.ml.aiopsLabs.title',
{
defaultMessage: 'Aiops labs',
}
);
export const OVERVIEW_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.overview.title',
{
defaultMessage: 'Overview',
}
);
export const OVERVIEW_DESC = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.overview.desc',
{
defaultMessage: 'Overview page',
}
);
export const OVERVIEW_TITLE = i18n.translate('xpack.securitySolution.navLinks.ml.overview.title', {
defaultMessage: 'Overview',
});
export const OVERVIEW_DESC = i18n.translate('xpack.securitySolution.navLinks.ml.overview.desc', {
defaultMessage: 'Overview page',
});
export const NOTIFICATIONS_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.notifications.title',
'xpack.securitySolution.navLinks.ml.notifications.title',
{
defaultMessage: 'Notifications',
}
);
export const NOTIFICATIONS_DESC = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.notifications.desc',
'xpack.securitySolution.navLinks.ml.notifications.desc',
{
defaultMessage: 'Notifications page',
}
);
export const MEMORY_USAGE_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.memoryUsage.title',
'xpack.securitySolution.navLinks.ml.memoryUsage.title',
{
defaultMessage: 'Memory usage',
}
);
export const MEMORY_USAGE_DESC = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.memoryUsage.desc',
'xpack.securitySolution.navLinks.ml.memoryUsage.desc',
{
defaultMessage: 'Memory usage page',
}
);
export const ANOMALY_DETECTION_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.anomalyDetection.title',
'xpack.securitySolution.navLinks.ml.anomalyDetection.title',
{
defaultMessage: 'Jobs',
}
);
export const ANOMALY_DETECTION_DESC = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.anomalyDetection.desc',
'xpack.securitySolution.navLinks.ml.anomalyDetection.desc',
{
defaultMessage: 'Jobs page',
}
);
export const ANOMALY_EXPLORER_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.anomalyExplorer.title',
'xpack.securitySolution.navLinks.ml.anomalyExplorer.title',
{
defaultMessage: 'Anomaly explorer',
}
);
export const ANOMALY_EXPLORER_DESC = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.anomalyExplorer.desc',
'xpack.securitySolution.navLinks.ml.anomalyExplorer.desc',
{
defaultMessage: 'Anomaly explorer page',
}
);
export const SINGLE_METRIC_VIEWER_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.singleMetricViewer.title',
'xpack.securitySolution.navLinks.ml.singleMetricViewer.title',
{
defaultMessage: 'Single metric viewer',
}
);
export const SINGLE_METRIC_VIEWER_DESC = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.singleMetricViewer.desc',
'xpack.securitySolution.navLinks.ml.singleMetricViewer.desc',
{
defaultMessage: 'Single metric viewer page',
}
);
export const SETTINGS_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.settings.title',
{
defaultMessage: 'Settings',
}
);
export const SETTINGS_DESC = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.settings.desc',
{
defaultMessage: 'Settings page',
}
);
export const SETTINGS_TITLE = i18n.translate('xpack.securitySolution.navLinks.ml.settings.title', {
defaultMessage: 'Settings',
});
export const SETTINGS_DESC = i18n.translate('xpack.securitySolution.navLinks.ml.settings.desc', {
defaultMessage: 'Settings page',
});
export const DATA_FRAME_ANALYTICS_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.dataFrameAnalytics.title',
'xpack.securitySolution.navLinks.ml.dataFrameAnalytics.title',
{
defaultMessage: 'Jobs',
}
);
export const DATA_FRAME_ANALYTICS_DESC = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.dataFrameAnalytics.desc',
'xpack.securitySolution.navLinks.ml.dataFrameAnalytics.desc',
{
defaultMessage: 'Jobs page',
}
);
export const RESULT_EXPLORER_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.resultExplorer.title',
'xpack.securitySolution.navLinks.ml.resultExplorer.title',
{
defaultMessage: 'Result explorer',
}
);
export const RESULT_EXPLORER_DESC = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.resultExplorer.desc',
'xpack.securitySolution.navLinks.ml.resultExplorer.desc',
{
defaultMessage: 'Result explorer page',
}
);
export const ANALYTICS_MAP_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.analyticsMap.title',
'xpack.securitySolution.navLinks.ml.analyticsMap.title',
{
defaultMessage: 'Analytics map',
}
);
export const ANALYTICS_MAP_DESC = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.analyticsMap.desc',
'xpack.securitySolution.navLinks.ml.analyticsMap.desc',
{
defaultMessage: 'Analytics map page',
}
);
export const NODES_OVERVIEW_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.nodesOverview.title',
'xpack.securitySolution.navLinks.ml.nodesOverview.title',
{
defaultMessage: 'Trained models',
}
);
export const NODES_OVERVIEW_DESC = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.nodesOverview.desc',
'xpack.securitySolution.navLinks.ml.nodesOverview.desc',
{
defaultMessage: 'Trained models page',
}
);
export const NODES_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.nodes.title',
{
defaultMessage: 'Nodes',
}
);
export const NODES_DESC = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.nodes.desc',
{
defaultMessage: 'Nodes page',
}
);
export const NODES_TITLE = i18n.translate('xpack.securitySolution.navLinks.ml.nodes.title', {
defaultMessage: 'Nodes',
});
export const NODES_DESC = i18n.translate('xpack.securitySolution.navLinks.ml.nodes.desc', {
defaultMessage: 'Nodes page',
});
export const FILE_UPLOAD_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.fileUpload.title',
'xpack.securitySolution.navLinks.ml.fileUpload.title',
{
defaultMessage: 'File data visualizer',
}
);
export const FILE_UPLOAD_DESC = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.fileUpload.desc',
'xpack.securitySolution.navLinks.ml.fileUpload.desc',
{
defaultMessage: 'File data visualizer page',
}
);
export const INDEX_DATA_VISUALIZER_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.indexDataVisualizer.title',
'xpack.securitySolution.navLinks.ml.indexDataVisualizer.title',
{
defaultMessage: 'Data view data visualizer',
}
);
export const INDEX_DATA_VISUALIZER_DESC = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.indexDataVisualizer.desc',
'xpack.securitySolution.navLinks.ml.indexDataVisualizer.desc',
{
defaultMessage: 'Data view data visualizer page',
}
);
export const DATA_DRIFT_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.dataDrift.title',
'xpack.securitySolution.navLinks.ml.dataDrift.title',
{
defaultMessage: 'Data drift',
}
);
export const DATA_COMPARISON_DESC = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.dataDrift.desc',
'xpack.securitySolution.navLinks.ml.dataDrift.desc',
{
defaultMessage: 'Data drift page',
}
);
export const LOG_RATE_ANALYSIS_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.explainLogRateSpikes.title',
'xpack.securitySolution.navLinks.ml.explainLogRateSpikes.title',
{
defaultMessage: 'Log Rate Analysis',
}
);
export const LOG_RATE_ANALYSIS_DESC = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.explainLogRateSpikes.desc',
'xpack.securitySolution.navLinks.ml.explainLogRateSpikes.desc',
{
defaultMessage: 'Log Rate Analysis Page',
}
);
export const LOG_PATTERN_ANALYSIS_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.logPatternAnalysis.title',
'xpack.securitySolution.navLinks.ml.logPatternAnalysis.title',
{
defaultMessage: 'Log pattern analysis',
}
);
export const LOG_PATTERN_ANALYSIS_DESC = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.logPatternAnalysis.desc',
'xpack.securitySolution.navLinks.ml.logPatternAnalysis.desc',
{
defaultMessage: 'Log pattern analysis page',
}
);
export const CHANGE_POINT_DETECTIONS_TITLE = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.changePointDetections.title',
'xpack.securitySolution.navLinks.ml.changePointDetections.title',
{
defaultMessage: 'Change point detection',
}
);
export const CHANGE_POINT_DETECTIONS_DESC = i18n.translate(
'xpack.securitySolutionServerless.navLinks.ml.changePointDetections.desc',
'xpack.securitySolution.navLinks.ml.changePointDetections.desc',
{
defaultMessage: 'Change point detection page',
}

View file

@ -5,13 +5,12 @@
* 2.0.
*/
import type { LinkItem } from '@kbn/security-solution-plugin/public';
import { SecurityPageName } from '@kbn/security-solution-navigation';
import { ExternalPageName } from '../constants';
import type { ProjectNavigationLink } from '../types';
import * as i18n from './project_settings_translations';
import { SecurityPageName, ExternalPageName } from '@kbn/security-solution-navigation';
import type { LinkItem } from '../../../../common/links/types';
import type { SolutionNavLink } from '../../../../common/links';
import * as i18n from './settings_translations';
export const createProjectSettingsLinksFromManage = (manageLink: LinkItem): LinkItem[] => {
export const createSettingsLinksFromManage = (manageLink: LinkItem): LinkItem[] => {
const entityAnalyticsLink = manageLink.links?.find(
({ id }) => id === SecurityPageName.entityAnalyticsManagement
);
@ -25,22 +24,16 @@ export const createProjectSettingsLinksFromManage = (manageLink: LinkItem): Link
: [];
};
export const projectSettingsNavLinks: ProjectNavigationLink[] = [
export const settingsNavLinks: SolutionNavLink[] = [
{
id: ExternalPageName.management,
title: i18n.MANAGEMENT_TITLE,
isFooterLink: true,
},
{
id: ExternalPageName.integrationsSecurity,
title: i18n.INTEGRATIONS_TITLE,
},
{
id: ExternalPageName.cloudUsersAndRoles,
title: i18n.CLOUD_USERS_ROLES_TITLE,
},
{
id: ExternalPageName.cloudBilling,
title: i18n.CLOUD_BILLING_TITLE,
isFooterLink: true,
},
{
id: ExternalPageName.maps,

View file

@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
export const MANAGEMENT_TITLE = i18n.translate('xpack.securitySolution.navLinks.management.title', {
defaultMessage: 'Stack Management',
});
export const INTEGRATIONS_TITLE = i18n.translate(
'xpack.securitySolution.navLinks.projectSettings.integrations.title',
{
defaultMessage: 'Integrations',
}
);
export const MAPS_TITLE = i18n.translate(
'xpack.securitySolution.navLinks.projectSettings.maps.title',
{
defaultMessage: 'Maps',
}
);
export const MAPS_DESCRIPTION = i18n.translate(
'xpack.securitySolution.navLinks.projectSettings.maps.description',
{
defaultMessage:
'Analyze geospatial data and identify geo patterns in multiple layers and indices.',
}
);
export const VISUALIZE_TITLE = i18n.translate(
'xpack.securitySolution.navLinks.projectSettings.visualize.title',
{
defaultMessage: 'Visualize library',
}
);
export const VISUALIZE_DESCRIPTION = i18n.translate(
'xpack.securitySolution.navLinks.projectSettings.visualize.description',
{
defaultMessage: 'Manage visualization library. Create, edit, and share visualizations.',
}
);

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { partition } from 'lodash/fp';
import { i18n } from '@kbn/i18n';
import type {
AppDeepLinkId,
NodeDefinition,
@ -18,28 +17,16 @@ import {
isTitleLinkCategory,
isAccordionLinkCategory,
} from '@kbn/security-solution-navigation';
import type { ProjectNavigationLink, ProjectPageName } from '../links/types';
import { getNavLinkIdFromProjectPageName, isBottomNavItemId, isCloudLink } from '../links/util';
import { isBreadcrumbHidden } from './utils';
import { ExternalPageName } from '../links/constants';
const SECURITY_PROJECT_TITLE = i18n.translate(
'xpack.securitySolutionServerless.nav.solution.title',
{
defaultMessage: 'Security',
}
);
import type { SolutionPageName, SolutionLinkCategory, SolutionNavLink } from '../../common/links';
import { getNavLinkIdFromSolutionPageName, isBreadcrumbHidden } from './util';
import { SOLUTION_NAME } from '../../common/translations';
export const formatNavigationTree = (
projectNavLinks: ProjectNavigationLink[],
bodyCategories: Readonly<Array<LinkCategory<ProjectPageName>>>,
footerCategories: Readonly<Array<LinkCategory<ProjectPageName>>>
solutionNavLinks: SolutionNavLink[],
bodyCategories: Readonly<SolutionLinkCategory[]>,
footerCategories: Readonly<SolutionLinkCategory[]>
): NavigationTreeDefinition => {
const [bodyNavItems, footerNavItems] = partition(
({ id }) => !isBottomNavItemId(id),
projectNavLinks
);
const [footerNavItems, bodyNavItems] = partition('isFooterLink', solutionNavLinks);
const bodyChildren = addMainLinksPanelOpenerProp(
formatNodesFromLinks(bodyNavItems, bodyCategories)
);
@ -47,8 +34,8 @@ export const formatNavigationTree = (
body: [
{
type: 'navGroup',
id: 'security_project_nav',
title: SECURITY_PROJECT_TITLE,
id: 'security_solution_nav',
title: SOLUTION_NAME,
icon: 'logoSecurity',
breadcrumbStatus: 'hidden',
defaultIsCollapsed: false,
@ -63,23 +50,23 @@ export const formatNavigationTree = (
// Body
const formatNodesFromLinks = (
projectNavLinks: ProjectNavigationLink[],
parentCategories?: Readonly<Array<LinkCategory<ProjectPageName>>>
solutionNavLinks: SolutionNavLink[],
parentCategories?: Readonly<Array<LinkCategory<SolutionPageName>>>
): NodeDefinition[] => {
const nodes: NodeDefinition[] = [];
if (parentCategories?.length) {
parentCategories.forEach((category) => {
nodes.push(...formatNodesFromLinksWithCategory(projectNavLinks, category));
nodes.push(...formatNodesFromLinksWithCategory(solutionNavLinks, category));
}, []);
} else {
nodes.push(...formatNodesFromLinksWithoutCategory(projectNavLinks));
nodes.push(...formatNodesFromLinksWithoutCategory(solutionNavLinks));
}
return nodes;
};
const formatNodesFromLinksWithCategory = (
projectNavLinks: ProjectNavigationLink[],
category: LinkCategory<ProjectPageName>
solutionNavLinks: SolutionNavLink[],
category: LinkCategory<SolutionPageName>
): NodeDefinition[] => {
if (!category?.linkIds) {
return [];
@ -87,9 +74,9 @@ const formatNodesFromLinksWithCategory = (
if (category.linkIds) {
const children = category.linkIds.reduce<NodeDefinition[]>((acc, linkId) => {
const projectNavLink = projectNavLinks.find(({ id }) => id === linkId);
if (projectNavLink != null) {
acc.push(createNodeFromProjectNavLink(projectNavLink));
const solutionNavLink = solutionNavLinks.find(({ id }) => id === linkId);
if (solutionNavLink != null) {
acc.push(createNodeFromSolutionNavLink(solutionNavLink));
}
return acc;
}, []);
@ -112,13 +99,13 @@ const formatNodesFromLinksWithCategory = (
};
const formatNodesFromLinksWithoutCategory = (
projectNavLinks: ProjectNavigationLink[]
solutionNavLinks: SolutionNavLink[]
): NodeDefinition[] =>
projectNavLinks.map((projectNavLink) => createNodeFromProjectNavLink(projectNavLink));
solutionNavLinks.map((solutionNavLink) => createNodeFromSolutionNavLink(solutionNavLink));
const createNodeFromProjectNavLink = (projectNavLink: ProjectNavigationLink): NodeDefinition => {
const { id, title, links, categories, disabled } = projectNavLink;
const link = getNavLinkIdFromProjectPageName(id);
const createNodeFromSolutionNavLink = (solutionNavLink: SolutionNavLink): NodeDefinition => {
const { id, title, links, categories, disabled } = solutionNavLink;
const link = getNavLinkIdFromSolutionPageName(id);
const node: NodeDefinition = {
id,
link: link as AppDeepLinkId,
@ -135,8 +122,8 @@ const createNodeFromProjectNavLink = (projectNavLink: ProjectNavigationLink): No
// Footer
const formatFooterNodesFromLinks = (
projectNavLinks: ProjectNavigationLink[],
parentCategories?: Readonly<Array<LinkCategory<ProjectPageName>>>
solutionNavLinks: SolutionNavLink[],
parentCategories?: Readonly<Array<LinkCategory<SolutionPageName>>>
): RootNavigationItemDefinition[] => {
const nodes: RootNavigationItemDefinition[] = [];
if (parentCategories?.length) {
@ -144,13 +131,13 @@ const formatFooterNodesFromLinks = (
if (isSeparatorLinkCategory(category)) {
nodes.push(
...category.linkIds.reduce<RootNavigationItemDefinition[]>((acc, linkId) => {
const projectNavLink = projectNavLinks.find(({ id }) => id === linkId);
if (projectNavLink != null) {
const solutionNavLink = solutionNavLinks.find(({ id }) => id === linkId);
if (solutionNavLink != null) {
acc.push({
type: 'navItem',
link: getNavLinkIdFromProjectPageName(projectNavLink.id) as AppDeepLinkId,
title: projectNavLink.title,
icon: projectNavLink.sideNavIcon,
link: getNavLinkIdFromSolutionPageName(solutionNavLink.id) as AppDeepLinkId,
title: solutionNavLink.title,
icon: solutionNavLink.sideNavIcon,
});
}
return acc;
@ -166,18 +153,11 @@ const formatFooterNodesFromLinks = (
breadcrumbStatus: 'hidden',
children:
category.linkIds?.reduce<NodeDefinition[]>((acc, linkId) => {
const projectNavLink = projectNavLinks.find(({ id }) => id === linkId);
if (projectNavLink != null) {
const solutionNavLink = solutionNavLinks.find(({ id }) => id === linkId);
if (solutionNavLink != null) {
acc.push({
title: projectNavLink.title,
...(isCloudLink(projectNavLink.id)
? {
cloudLink: getCloudLink(projectNavLink.id),
openInNewTab: true,
}
: {
link: getNavLinkIdFromProjectPageName(projectNavLink.id) as AppDeepLinkId,
}),
title: solutionNavLink.title,
link: getNavLinkIdFromSolutionPageName(solutionNavLink.id) as AppDeepLinkId,
});
}
return acc;
@ -212,15 +192,3 @@ const addMainLinksPanelOpenerProp = (nodes: NodeDefinition[]): NodeDefinition[]
}
return node;
});
/** Returns the cloud link entry the default navigation expects */
const getCloudLink = (id: ProjectPageName) => {
switch (id) {
case ExternalPageName.cloudUsersAndRoles:
return 'userAndRoles';
case ExternalPageName.cloudBilling:
return 'billingAndSub';
default:
return undefined;
}
};

View file

@ -0,0 +1,84 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import type { PanelContentProvider } from '@kbn/shared-ux-chrome-navigation';
import type { PanelComponentProps } from '@kbn/shared-ux-chrome-navigation/src/ui/components/panel/types';
import { SolutionSideNavPanelContent } from '@kbn/security-solution-side-nav/panel';
import useObservable from 'react-use/lib/useObservable';
import type { Observable } from 'rxjs';
import { map } from 'rxjs';
import type { NavigationTreeDefinition } from '@kbn/core-chrome-browser';
import { NavigationProvider } from '@kbn/security-solution-navigation';
import type { CoreStart } from '@kbn/core/public';
import { usePanelSideNavItems } from './use_panel_side_nav_items';
import { CATEGORIES, FOOTER_CATEGORIES } from './categories';
import { formatNavigationTree } from './navigation_tree';
import type { SolutionNavLinks$ } from '../../common/links';
import { navLinks$ } from '../../common/links/nav_links';
export const withNavigationProvider = <T extends object>(
Component: React.ComponentType<T>,
core: CoreStart
) =>
function WithNavigationProvider(props: T) {
return (
<NavigationProvider core={core}>
<Component {...props} />
</NavigationProvider>
);
};
const getPanelContent = (
core: CoreStart,
solutionNavLinks$: SolutionNavLinks$
): React.FC<PanelComponentProps> => {
const PanelContentProvider: React.FC<PanelComponentProps> = React.memo(
function PanelContentProvider({ selectedNode: { id: linkId }, closePanel }) {
const solutionNavLinks = useObservable(solutionNavLinks$, []);
const currentPanelItem = solutionNavLinks.find((item) => item.id === linkId);
const { title = '', links = [], categories } = currentPanelItem ?? {};
const items = usePanelSideNavItems(links);
if (items.length === 0) {
return null;
}
return (
<SolutionSideNavPanelContent
title={title}
items={items}
categories={categories}
onClose={closePanel}
/>
);
}
);
return withNavigationProvider(PanelContentProvider, core);
};
export interface SolutionNavigation {
navigationTree$: Observable<NavigationTreeDefinition>;
panelContentProvider: PanelContentProvider;
}
export const getSolutionNavigation = (core: CoreStart): SolutionNavigation => {
const panelContent = getPanelContent(core, navLinks$);
const panelContentProvider: PanelContentProvider = (id: string) => {
// Stack Management uses the default panel content
if (!id.endsWith('.stack_management')) {
return { content: panelContent };
}
};
const navigationTree$ = navLinks$.pipe(
map((solutionNavLinks) => formatNavigationTree(solutionNavLinks, CATEGORIES, FOOTER_CATEGORIES))
);
return { navigationTree$, panelContentProvider };
};

View file

@ -8,7 +8,6 @@
import { renderHook } from '@testing-library/react-hooks';
import { usePanelSideNavItems } from './use_panel_side_nav_items';
import { SecurityPageName } from '@kbn/security-solution-navigation';
import { ExternalPageName } from '../links/constants';
jest.mock('@kbn/security-solution-navigation/src/navigation');
@ -90,6 +89,7 @@ describe('usePanelSideNavItems', () => {
id: SecurityPageName.landing,
title: 'Get Started',
sideNavIcon: 'launch',
isFooterLink: true,
},
],
});
@ -106,30 +106,4 @@ describe('usePanelSideNavItems', () => {
},
]);
});
it('should openInNewTab for external (cloud) links', async () => {
const { result } = renderHook(usePanelSideNavItems, {
initialProps: [
{
id: ExternalPageName.cloudUsersAndRoles,
externalUrl: 'https://cloud.elastic.co/users_roles',
title: 'Users & Roles',
sideNavIcon: 'someicon',
},
],
});
const items = result.current;
expect(items).toEqual([
{
id: ExternalPageName.cloudUsersAndRoles,
href: 'https://cloud.elastic.co/users_roles',
label: 'Users & Roles',
openInNewTab: true,
iconType: 'someicon',
position: 'bottom',
},
]);
});
});

View file

@ -6,25 +6,19 @@
*/
import { useCallback, useMemo } from 'react';
import { type NavigationLink } from '@kbn/security-solution-navigation';
import { useGetLinkProps } from '@kbn/security-solution-navigation/links';
import { SolutionSideNavItemPosition } from '@kbn/security-solution-side-nav';
import type { ProjectSideNavItem } from './types';
import type { ProjectNavigationLink, ProjectPageName } from '../links/types';
import { isBottomNavItemId } from '../links/util';
import type { SolutionSideNavItem, SolutionNavLink } from '../../common/links';
type GetLinkProps = (link: NavigationLink) => {
href: string & Partial<ProjectSideNavItem>;
type GetLinkProps = (link: SolutionNavLink) => {
href: string & Partial<SolutionSideNavItem>;
};
/**
* Formats generic navigation links into the shape expected by the `SolutionSideNav`
*/
const formatLink = (
navLink: NavigationLink<ProjectPageName>,
getLinkProps: GetLinkProps
): ProjectSideNavItem => {
const items = navLink.links?.reduce<ProjectSideNavItem[]>((acc, current) => {
const formatLink = (navLink: SolutionNavLink, getLinkProps: GetLinkProps): SolutionSideNavItem => {
const items = navLink.links?.reduce<SolutionSideNavItem[]>((acc, current) => {
if (!current.disabled) {
acc.push({
id: current.id,
@ -42,7 +36,7 @@ const formatLink = (
id: navLink.id,
label: navLink.title,
iconType: navLink.sideNavIcon,
position: isBottomNavItemId(navLink.id)
position: navLink.isFooterLink
? SolutionSideNavItemPosition.bottom
: SolutionSideNavItemPosition.top,
...getLinkProps(navLink),
@ -54,26 +48,17 @@ const formatLink = (
/**
* Returns all the formatted SideNavItems for the panel, including external links
*/
export const usePanelSideNavItems = (navLinks: ProjectNavigationLink[]): ProjectSideNavItem[] => {
export const usePanelSideNavItems = (navLinks: SolutionNavLink[]): SolutionSideNavItem[] => {
const getKibanaLinkProps = useGetLinkProps();
const getLinkProps = useCallback<GetLinkProps>(
(link) => {
if (link.externalUrl) {
return {
href: link.externalUrl,
openInNewTab: true,
};
} else {
return getKibanaLinkProps({ id: link.id });
}
},
(link) => getKibanaLinkProps({ id: link.id }),
[getKibanaLinkProps]
);
return useMemo(
() =>
navLinks.reduce<ProjectSideNavItem[]>((items, navLink) => {
navLinks.reduce<SolutionSideNavItem[]>((items, navLink) => {
if (!navLink.disabled) {
items.push(formatLink(navLink, getLinkProps));
}

View file

@ -0,0 +1,57 @@
/*
* 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 { SolutionPageName } from '../../common/links';
import { getNavLinkIdFromSolutionPageName, getSolutionPageNameFromNavLinkId } from './util';
describe('util', () => {
describe('getNavLinkIdFromSolutionPageName', () => {
it('should return the correct navLink id for security pages', () => {
expect(getNavLinkIdFromSolutionPageName('administration' as SolutionPageName)).toEqual(
'securitySolutionUI:administration'
);
});
it('should return the correct navLink id for app root', () => {
expect(getNavLinkIdFromSolutionPageName('discover:' as SolutionPageName)).toEqual('discover');
});
it('should return the correct navLink id for app nested pages', () => {
expect(getNavLinkIdFromSolutionPageName('ml:overview' as SolutionPageName)).toEqual(
'ml:overview'
);
});
it('should return the correct navLink id pages with custom path', () => {
expect(
getNavLinkIdFromSolutionPageName('integrations:/browse/security' as SolutionPageName)
).toEqual('integrations');
});
it('should return the correct navLink id for nested page custom path', () => {
expect(
getNavLinkIdFromSolutionPageName('fleet:agents/test/path' as SolutionPageName)
).toEqual('fleet:agents');
});
});
describe('getSolutionPageNameFromNavLinkId', () => {
it('should return the correct solution page name for security pages', () => {
expect(getSolutionPageNameFromNavLinkId('securitySolutionUI:administration')).toEqual(
'administration'
);
});
it('should return the correct solution page name for app root', () => {
expect(getSolutionPageNameFromNavLinkId('discover')).toEqual('discover:');
});
it('should return the correct solution page name for app nested pages', () => {
expect(getSolutionPageNameFromNavLinkId('ml:overview')).toEqual('ml:overview');
});
});
});

View file

@ -5,15 +5,27 @@
* 2.0.
*/
import { SecurityPageName } from '@kbn/security-solution-navigation';
import { ExternalPageName } from '../links/constants';
import type { ProjectPageName } from '../links/types';
import { ExternalPageName, SecurityPageName } from '@kbn/security-solution-navigation';
import { APP_UI_ID } from '../../../common';
import type { SolutionPageName } from '../../common/links';
export const getNavLinkIdFromSolutionPageName = (solutionPageName: SolutionPageName): string => {
const cleanId = solutionPageName.replace(/\/(.*)$/, ''); // remove any trailing path
const fullId = cleanId.includes(':') ? cleanId : `${APP_UI_ID}:${cleanId}`; // add the Security appId if not defined
return fullId.replace(/:$/, ''); // clean trailing separator to app root links to contain the appId alone
};
export const getSolutionPageNameFromNavLinkId = (navLinkId: string): SolutionPageName => {
const cleanId = navLinkId.includes(':') ? navLinkId : `${navLinkId}:`; // add trailing separator to app root links that contain the appId alone
const fullId = cleanId.replace(`${APP_UI_ID}:`, ''); // remove Security appId if present
return fullId as SolutionPageName;
};
// 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<ProjectPageName>([
const HIDDEN_BREADCRUMBS = new Set<SolutionPageName>([
SecurityPageName.networkDns,
SecurityPageName.networkHttp,
SecurityPageName.networkTls,
@ -30,7 +42,7 @@ const HIDDEN_BREADCRUMBS = new Set<ProjectPageName>([
SecurityPageName.sessions,
]);
export const isBreadcrumbHidden = (id: ProjectPageName): boolean =>
export const isBreadcrumbHidden = (id: SolutionPageName): boolean =>
HIDDEN_BREADCRUMBS.has(id) ||
/* management sub-pages set their breadcrumbs themselves, the main Management breadcrumb is configured with our navigationTree definition */
(id.startsWith(ExternalPageName.management) && id !== ExternalPageName.management);

View file

@ -4,8 +4,8 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { links } from './app_links';
import type { LinkItem, AppLinkItems } from './types';
import { appLinks } from './app_links';
import type { LinkItem, AppLinkItems } from './common/links/types';
const traverse = (linksItems: AppLinkItems, fn: (link: LinkItem) => void) => {
linksItems.forEach((link) => {
@ -18,7 +18,7 @@ const traverse = (linksItems: AppLinkItems, fn: (link: LinkItem) => void) => {
describe('Security app links', () => {
it('should only contain static paths', () => {
traverse(links, (link) => {
traverse(appLinks, (link) => {
expect(link.path).not.toContain('/:');
});
});

View file

@ -5,20 +5,23 @@
* 2.0.
*/
import type { CoreStart } from '@kbn/core/public';
import type { AppLinkItems } from './types';
import { indicatorsLinks } from '../../threat_intelligence/links';
import { links as alertsLinks } from '../../detections/links';
import { links as rulesLinks } from '../../rules/links';
import { links as timelinesLinks } from '../../timelines/links';
import { links as casesLinks } from '../../cases/links';
import { links as managementLinks, getManagementFilteredLinks } from '../../management/links';
import { exploreLinks } from '../../explore/links';
import { gettingStartedLinks } from '../../overview/links';
import { findingsLinks } from '../../cloud_security_posture/links';
import type { StartPlugins } from '../../types';
import { dashboardsLinks } from '../../dashboards/links';
import type { AppLinkItems } from './common/links/types';
import { indicatorsLinks } from './threat_intelligence/links';
import { links as alertsLinks } from './detections/links';
import { links as rulesLinks } from './rules/links';
import { links as timelinesLinks } from './timelines/links';
import { links as casesLinks } from './cases/links';
import { links as managementLinks, getManagementFilteredLinks } from './management/links';
import { exploreLinks } from './explore/links';
import { gettingStartedLinks } from './overview/links';
import { findingsLinks } from './cloud_security_posture/links';
import type { StartPlugins } from './types';
import { dashboardsLinks } from './dashboards/links';
export const links: AppLinkItems = Object.freeze([
// TODO: remove after rollout https://github.com/elastic/kibana/issues/179572
export { solutionAppLinksSwitcher } from './app/solution_navigation/links/app_links';
export const appLinks: AppLinkItems = Object.freeze([
dashboardsLinks,
alertsLinks,
findingsLinks,

View file

@ -0,0 +1,58 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { LandingLinksIconsGroups } from '@kbn/security-solution-navigation/landing_links';
import { SecurityPageName, ExternalPageName } from '@kbn/security-solution-navigation';
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
import { EuiCallOut, EuiPageHeader, EuiSpacer, useEuiTheme } from '@elastic/eui';
import { LinkButton } from '@kbn/security-solution-navigation/links';
import { useRootNavLink } from '../common/links/nav_links';
const INTEGRATIONS_CALLOUT_TITLE = i18n.translate(
'xpack.securitySolution.assets.integrationsCallout.title',
{ defaultMessage: 'Integrations' }
);
const INTEGRATIONS_CALLOUT_DESCRIPTION = i18n.translate(
'xpack.securitySolution.assets.integrationsCallout.content',
{ defaultMessage: 'Choose an integration to start collecting and analyzing your data.' }
);
const INTEGRATIONS_CALLOUT_BUTTON_TEXT = i18n.translate(
'xpack.securitySolution.assets.integrationsCallout.buttonText',
{ defaultMessage: 'Browse integrations' }
);
export const Assets: React.FC = () => {
const { euiTheme } = useEuiTheme();
const link = useRootNavLink(SecurityPageName.assets);
const { links = [], title } = link ?? {};
return (
<KibanaPageTemplate restrictWidth={false} contentBorder={false} grow={true}>
<KibanaPageTemplate.Section>
<EuiPageHeader pageTitle={title} />
<EuiSpacer size="l" />
<EuiSpacer size="xl" />
<LandingLinksIconsGroups items={links} />
<EuiSpacer size="l" />
<EuiSpacer size="l" />
<EuiCallOut
title={INTEGRATIONS_CALLOUT_TITLE}
color="primary"
iconType="cluster"
style={{ borderRadius: euiTheme.border.radius.medium }}
>
<p>{INTEGRATIONS_CALLOUT_DESCRIPTION}</p>
<LinkButton id={ExternalPageName.integrationsSecurity} fill>
{INTEGRATIONS_CALLOUT_BUTTON_TEXT}
</LinkButton>
</EuiCallOut>
</KibanaPageTemplate.Section>
</KibanaPageTemplate>
);
};

View file

@ -5,7 +5,15 @@
* 2.0.
*/
import type { SolutionSideNavItem } from '@kbn/security-solution-side-nav';
import type { ProjectPageName } from '../links/types';
import type { SecuritySubPlugin } from '../app/types';
import { routes } from './routes';
export type ProjectSideNavItem = SolutionSideNavItem<ProjectPageName>;
export class Assets {
public setup() {}
public start(): SecuritySubPlugin {
return {
routes,
};
}
}

View file

@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { SecurityPageName } from '@kbn/security-solution-navigation';
import { ASSETS_PATH } from '../../common/constants';
import type { SecuritySubPluginRoutes } from '../app/types';
import { Assets } from './assets';
import { PluginTemplateWrapper } from '../common/components/plugin_template_wrapper';
import { SecurityRoutePageWrapper } from '../common/components/security_route_page_wrapper';
const AssetsPage = React.memo(function AssetsPage() {
return (
<PluginTemplateWrapper>
<SecurityRoutePageWrapper pageName={SecurityPageName.assets} redirectOnMissing>
<Assets />
</SecurityRoutePageWrapper>
</PluginTemplateWrapper>
);
});
export const routes: SecuritySubPluginRoutes = [
{
path: ASSETS_PATH,
component: AssetsPage,
},
];

View file

@ -53,9 +53,9 @@ jest.mock('../../../links', () => ({
getAncestorLinksInfo: (id: string) => [{ id }],
}));
const mockUseNavLinks = jest.fn();
const mockUseSecurityInternalNavLinks = jest.fn();
jest.mock('../../../links/nav_links', () => ({
useNavLinks: () => mockUseNavLinks(),
useSecurityInternalNavLinks: () => mockUseSecurityInternalNavLinks(),
}));
jest.mock('../../links', () => ({
useGetSecuritySolutionLinkProps:
@ -82,14 +82,14 @@ const renderNav = () =>
describe('SecuritySideNav', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseNavLinks.mockReturnValue([alertsNavLink, settingsNavLink]);
mockUseSecurityInternalNavLinks.mockReturnValue([alertsNavLink, settingsNavLink]);
useKibana().services.chrome.hasHeaderBanner$ = jest.fn(() =>
new BehaviorSubject(false).asObservable()
);
});
it('should render main items', () => {
mockUseNavLinks.mockReturnValue([alertsNavLink]);
mockUseSecurityInternalNavLinks.mockReturnValue([alertsNavLink]);
renderNav();
expect(mockSolutionSideNav).toHaveBeenCalledWith({
selectedId: SecurityPageName.alerts,
@ -107,7 +107,7 @@ describe('SecuritySideNav', () => {
});
it('should render the loader if items are still empty', () => {
mockUseNavLinks.mockReturnValue([]);
mockUseSecurityInternalNavLinks.mockReturnValue([]);
const result = renderNav();
expect(result.getByTestId('sideNavLoader')).toBeInTheDocument();
expect(mockSolutionSideNav).not.toHaveBeenCalled();
@ -124,7 +124,7 @@ describe('SecuritySideNav', () => {
});
it('should render footer items', () => {
mockUseNavLinks.mockReturnValue([settingsNavLink]);
mockUseSecurityInternalNavLinks.mockReturnValue([settingsNavLink]);
renderNav();
expect(mockSolutionSideNav).toHaveBeenCalledWith(
expect.objectContaining({
@ -150,7 +150,10 @@ describe('SecuritySideNav', () => {
});
it('should not render disabled items', () => {
mockUseNavLinks.mockReturnValue([{ ...alertsNavLink, disabled: true }, settingsNavLink]);
mockUseSecurityInternalNavLinks.mockReturnValue([
{ ...alertsNavLink, disabled: true },
settingsNavLink,
]);
renderNav();
expect(mockSolutionSideNav).toHaveBeenCalledWith(
expect.objectContaining({
@ -164,7 +167,7 @@ describe('SecuritySideNav', () => {
});
it('should render get started item', () => {
mockUseNavLinks.mockReturnValue([
mockUseSecurityInternalNavLinks.mockReturnValue([
{ id: SecurityPageName.landing, title: 'Get started', sideNavIcon: 'launch' },
]);
renderNav();

View file

@ -14,11 +14,11 @@ import {
} from '@kbn/security-solution-side-nav';
import useObservable from 'react-use/lib/useObservable';
import { SecurityPageName } from '../../../../app/types';
import type { NavigationLink } from '../../../links';
import type { SecurityNavLink } from '../../../links';
import { getAncestorLinksInfo } from '../../../links';
import { useRouteSpy } from '../../../utils/route/use_route_spy';
import { useGetSecuritySolutionLinkProps, type GetSecuritySolutionLinkProps } from '../../links';
import { useNavLinks } from '../../../links/nav_links';
import { useSecurityInternalNavLinks } from '../../../links/nav_links';
import { useShowTimeline } from '../../../utils/timeline/use_show_timeline';
import { useIsPolicySettingsBarVisible } from '../../../../management/pages/policy/view/policy_hooks';
import { track } from '../../../lib/telemetry';
@ -39,7 +39,7 @@ const isGetStartedNavItem = (id: SecurityPageName) => id === SecurityPageName.la
* Formats generic navigation links into the shape expected by the `SolutionSideNav`
*/
const formatLink = (
navLink: NavigationLink,
navLink: SecurityNavLink,
getSecuritySolutionLinkProps: GetSecuritySolutionLinkProps
): SolutionSideNavItem => ({
id: navLink.id,
@ -69,7 +69,7 @@ const formatLink = (
* Formats the get started navigation links into the shape expected by the `SolutionSideNav`
*/
const formatGetStartedLink = (
navLink: NavigationLink,
navLink: SecurityNavLink,
getSecuritySolutionLinkProps: GetSecuritySolutionLinkProps
): SolutionSideNavItem => ({
id: navLink.id,
@ -84,7 +84,7 @@ const formatGetStartedLink = (
* Returns the formatted `items` and `footerItems` to be rendered in the navigation
*/
const useSolutionSideNavItems = () => {
const navLinks = useNavLinks();
const navLinks = useSecurityInternalNavLinks();
const getSecuritySolutionLinkProps = useGetSecuritySolutionLinkProps(); // adds href and onClick props
const sideNavItems = useMemo(() => {

View file

@ -5,22 +5,27 @@
* 2.0.
*/
import React from 'react';
import { render } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import { of } from 'rxjs';
import { useSecuritySolutionNavigation } from './use_security_solution_navigation';
const mockUseBreadcrumbsNav = jest.fn();
jest.mock('../breadcrumbs', () => ({
useBreadcrumbsNav: () => jest.fn(),
useBreadcrumbsNav: () => mockUseBreadcrumbsNav(),
}));
const mockIsSideNavEnabled = jest.fn(() => true);
const mockSecuritySideNav = jest.fn(() => <div data-test-subj="SecuritySideNav" />);
jest.mock('../security_side_nav', () => ({
SecuritySideNav: () => mockSecuritySideNav(),
}));
const mockGetChromeStyle$ = jest.fn().mockReturnValue(of('classic'));
jest.mock('../../../lib/kibana/kibana_react', () => {
return {
useKibana: () => ({
services: {
configSettings: {
sideNavEnabled: mockIsSideNavEnabled(),
},
},
services: { chrome: { getChromeStyle$: () => mockGetChromeStyle$() } },
}),
};
});
@ -29,23 +34,57 @@ describe('Security Solution Navigation', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('when classic navigation is enabled', () => {
beforeAll(() => {
mockGetChromeStyle$.mockReturnValue(of('classic'));
});
it('should return proper navigation props', () => {
const { result } = renderHook(useSecuritySolutionNavigation);
expect(result.current).toEqual(
expect.objectContaining({
it('should return proper navigation props', async () => {
const { result } = renderHook(useSecuritySolutionNavigation);
expect(result.current).toEqual(
expect.objectContaining({
canBeCollapsed: true,
name: 'Security',
icon: 'logoSecurity',
closeFlyoutButtonPosition: 'inside',
})
);
// check regular props
expect(result.current).toEqual({
canBeCollapsed: true,
name: 'Security',
icon: 'logoSecurity',
children: expect.anything(),
closeFlyoutButtonPosition: 'inside',
})
);
expect(result.current?.children).toBeDefined();
});
// check rendering of SecuritySideNav children
const { findByTestId } = render(<>{result.current?.children}</>);
expect(mockSecuritySideNav).toHaveBeenCalled();
expect(await findByTestId('SecuritySideNav')).toBeInTheDocument();
});
it('should initialize breadcrumbs', () => {
renderHook(useSecuritySolutionNavigation);
expect(mockUseBreadcrumbsNav).toHaveBeenCalled();
});
});
it('should return undefined props when disabled', () => {
mockIsSideNavEnabled.mockReturnValueOnce(false);
const { result } = renderHook(useSecuritySolutionNavigation);
expect(result.current).toEqual(undefined);
describe('when solution navigation is enabled', () => {
beforeAll(() => {
mockGetChromeStyle$.mockReturnValue(of('project'));
});
it('should return undefined props when disabled', () => {
const { result } = renderHook(useSecuritySolutionNavigation);
expect(result.current).toEqual(undefined);
});
it('should initialize breadcrumbs', () => {
renderHook(useSecuritySolutionNavigation);
expect(mockUseBreadcrumbsNav).toHaveBeenCalled();
});
});
});

View file

@ -11,9 +11,10 @@
* 2.0.
*/
import React from 'react';
import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import type { KibanaPageTemplateProps } from '@kbn/shared-ux-page-kibana-template';
import { useObservable } from 'react-use';
import { useKibana } from '../../../lib/kibana';
import { useBreadcrumbsNav } from '../breadcrumbs';
import { SecuritySideNav } from '../security_side_nav';
@ -23,11 +24,14 @@ const translatedNavTitle = i18n.translate('xpack.securitySolution.navigation.mai
});
export const useSecuritySolutionNavigation = (): KibanaPageTemplateProps['solutionNav'] => {
const { sideNavEnabled } = useKibana().services.configSettings;
const { chrome } = useKibana().services;
const chromeStyle$ = useMemo(() => chrome.getChromeStyle$(), [chrome]);
const chromeStyle = useObservable(chromeStyle$, 'classic');
useBreadcrumbsNav();
if (!sideNavEnabled) {
if (chromeStyle === 'project') {
// new shared-ux 'project' navigation enabled, return undefined to disable the 'classic' navigation
return undefined;
}

View file

@ -12,18 +12,30 @@ import { SecurityRoutePageWrapper } from '.';
import { SecurityPageName } from '../../../../common';
import { TestProviders } from '../../mock';
import { generateHistoryMock } from '../../utils/route/mocks';
import type { LinkInfo } from '../../links';
const mockUseLinkAuthorized = jest.fn();
const mockUseUpsellingPage = jest.fn();
const defaultLinkInfo: LinkInfo = {
id: SecurityPageName.exploreLanding,
title: 'test',
path: '/test',
};
const mockGetLink = jest.fn((): LinkInfo | undefined => defaultLinkInfo);
jest.mock('../../links', () => ({
useLinkAuthorized: () => mockUseLinkAuthorized(),
useLinkInfo: () => mockGetLink(),
}));
const mockUseUpsellingPage = jest.fn();
jest.mock('../../hooks/use_upselling', () => ({
useUpsellingPage: () => mockUseUpsellingPage(),
}));
const REDIRECT_COMPONENT_SUBJ = 'redirect-component';
const mockRedirect = jest.fn(() => <div data-test-subj={REDIRECT_COMPONENT_SUBJ} />);
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
Redirect: () => mockRedirect(),
}));
const TEST_COMPONENT_SUBJ = 'test-component';
const TestComponent = () => <div data-test-subj={TEST_COMPONENT_SUBJ} />;
const mockHistory = generateHistoryMock();
@ -35,48 +47,56 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => (
);
describe('SecurityRoutePageWrapper', () => {
it('renders children when authorized', () => {
mockUseLinkAuthorized.mockReturnValue(true);
it('should render children when authorized', () => {
mockGetLink.mockReturnValueOnce({ ...defaultLinkInfo }); // authorized
const { getByTestId } = render(
<SecurityRoutePageWrapper pageName={SecurityPageName.exploreLanding}>
<TestComponent />
</SecurityRoutePageWrapper>,
{ wrapper: Wrapper }
);
expect(getByTestId(TEST_COMPONENT_SUBJ)).toBeInTheDocument();
});
it('renders UpsellPage when unauthorized and UpsellPage is available', () => {
it('should render UpsellPage when unauthorized and UpsellPage is available', () => {
const TestUpsellPage = () => <div data-test-subj={'test-upsell-page'} />;
mockUseLinkAuthorized.mockReturnValue(false);
mockGetLink.mockReturnValueOnce({ ...defaultLinkInfo, unauthorized: true });
mockUseUpsellingPage.mockReturnValue(TestUpsellPage);
const { getByTestId } = render(
<SecurityRoutePageWrapper pageName={SecurityPageName.exploreLanding}>
<TestComponent />
</SecurityRoutePageWrapper>,
{ wrapper: Wrapper }
);
expect(getByTestId('test-upsell-page')).toBeInTheDocument();
});
it('renders NoPrivilegesPage when unauthorized and UpsellPage is unavailable', () => {
mockUseLinkAuthorized.mockReturnValue(false);
it('should render NoPrivilegesPage when unauthorized and UpsellPage is unavailable', () => {
mockGetLink.mockReturnValueOnce({ ...defaultLinkInfo, unauthorized: true });
mockUseUpsellingPage.mockReturnValue(undefined);
const { getByTestId } = render(
<SecurityRoutePageWrapper pageName={SecurityPageName.exploreLanding}>
<TestComponent />
</SecurityRoutePageWrapper>,
{ wrapper: Wrapper }
);
expect(getByTestId('noPrivilegesPage')).toBeInTheDocument();
});
});
// Write unit test for file /Users/pablo.nevesmachado/workspace/kibana/x-pack/plugins/security_solution/public/common/components/security_route_page_wrapper/index.tsx
it('should redirect when link missing and redirectOnMissing flag present', () => {
mockGetLink.mockReturnValueOnce(undefined);
const { getByTestId } = render(
<SecurityRoutePageWrapper pageName={SecurityPageName.exploreLanding} redirectOnMissing>
<TestComponent />
</SecurityRoutePageWrapper>,
{ wrapper: Wrapper }
);
expect(getByTestId(REDIRECT_COMPONENT_SUBJ)).toBeInTheDocument();
});
});

View file

@ -6,15 +6,17 @@
*/
import React from 'react';
import { Redirect } from 'react-router-dom';
import { TrackApplicationView } from '@kbn/usage-collection-plugin/public';
import type { SecurityPageName } from '../../../../common';
import { useLinkAuthorized } from '../../links';
import { useLinkInfo } from '../../links';
import { NoPrivilegesPage } from '../no_privileges';
import { useUpsellingPage } from '../../hooks/use_upselling';
import { SpyRoute } from '../../utils/route/spy_routes';
interface SecurityRoutePageWrapperProps {
pageName: SecurityPageName;
redirectOnMissing?: boolean;
}
/**
@ -37,14 +39,20 @@ interface SecurityRoutePageWrapperProps {
export const SecurityRoutePageWrapper: React.FC<SecurityRoutePageWrapperProps> = ({
children,
pageName,
redirectOnMissing,
}) => {
const isAuthorized = useLinkAuthorized(pageName);
const link = useLinkInfo(pageName);
const UpsellPage = useUpsellingPage(pageName);
const isAuthorized = link != null && !link.unauthorized;
if (isAuthorized) {
return <TrackApplicationView viewId={pageName}>{children}</TrackApplicationView>;
}
if (redirectOnMissing && link == null) {
return <Redirect to="" />; // redirects to the home page
}
if (UpsellPage) {
return (
<>

View file

@ -11,9 +11,24 @@ import type { StartPlugins } from '../../../types';
type GlobalServices = Pick<CoreStart, 'application' | 'http' | 'uiSettings' | 'notifications'> &
Pick<StartPlugins, 'data' | 'unifiedSearch' | 'expressions' | 'savedSearch'>;
/**
* This class is a singleton that holds references to core Kibana services.
* It is initialized during the plugin start lifecycle.
* Use with caution since it is not updated if services are changed after initialization.
* useKibana hook should be used in React components to access these services.
*/
export class KibanaServices {
/**
* Whether the environment is 'serverless' or 'traditional'
*/
private static buildFlavor?: string;
/**
* The current Kibana branch. e.g. 'main'
*/
private static kibanaBranch?: string;
/**
* The current Kibana version. e.g. '8.0.0' or '8.0.0-SNAPSHOT'
*/
private static kibanaVersion?: string;
private static prebuiltRulesPackageVersion?: string;
private static services?: GlobalServices;

View file

@ -6,13 +6,15 @@
*/
import type { Subject, Subscription } from 'rxjs';
import { combineLatestWith } from 'rxjs';
import type { AppDeepLink, AppUpdater, AppDeepLinkLocations } from '@kbn/core/public';
import { appLinks$ } from './links';
import type { AppLinkItems } from './types';
export type DeepLinksFormatter = (appLinks: AppLinkItems) => AppDeepLink[];
type DeepLinksFormatter = (appLinks: AppLinkItems) => AppDeepLink[];
const defaultDeepLinksFormatter: DeepLinksFormatter = (appLinks) =>
// TODO: remove after rollout https://github.com/elastic/kibana/issues/179572
const classicFormatter: DeepLinksFormatter = (appLinks) =>
appLinks.map((appLink) => {
const visibleIn: Set<AppDeepLinkLocations> = new Set(appLink.visibleIn ?? []);
if (!appLink.globalSearchDisabled) {
@ -30,7 +32,31 @@ const defaultDeepLinksFormatter: DeepLinksFormatter = (appLinks) =>
...(appLink.globalSearchKeywords != null ? { keywords: appLink.globalSearchKeywords } : {}),
...(appLink.links && appLink.links?.length
? {
deepLinks: defaultDeepLinksFormatter(appLink.links),
deepLinks: classicFormatter(appLink.links),
}
: {}),
};
return deepLink;
});
const solutionFormatter: DeepLinksFormatter = (appLinks) =>
appLinks.map((appLink) => {
const visibleIn: Set<AppDeepLinkLocations> = new Set(appLink.visibleIn ?? []);
if (!appLink.globalSearchDisabled) {
visibleIn.add('globalSearch');
}
if (!appLink.sideNavDisabled) {
visibleIn.add('sideNav');
}
const deepLink: AppDeepLink = {
id: appLink.id,
path: appLink.path,
title: appLink.title,
visibleIn: Array.from(visibleIn),
...(appLink.globalSearchKeywords != null ? { keywords: appLink.globalSearchKeywords } : {}),
...(appLink.links && appLink.links?.length
? {
deepLinks: solutionFormatter(appLink.links),
}
: {}),
};
@ -42,11 +68,15 @@ const defaultDeepLinksFormatter: DeepLinksFormatter = (appLinks) =>
*/
export const registerDeepLinksUpdater = (
appUpdater$: Subject<AppUpdater>,
formatter: DeepLinksFormatter = defaultDeepLinksFormatter
isSolutionNavigationEnabled$: Subject<boolean>
): Subscription => {
return appLinks$.subscribe((appLinks) => {
appUpdater$.next(() => ({
deepLinks: formatter(appLinks),
}));
});
return appLinks$
.pipe(combineLatestWith(isSolutionNavigationEnabled$))
.subscribe(([appLinks, isSolutionNavigationEnabled]) => {
appUpdater$.next(() => ({
deepLinks: isSolutionNavigationEnabled
? solutionFormatter(appLinks)
: classicFormatter(appLinks),
}));
});
};

View file

@ -57,8 +57,8 @@ const mockCapabilities = {
const fakePageId = 'fakePage';
const testFeatureflag = 'detectionResponseEnabled';
jest.mock('./app_links', () => {
const actual = jest.requireActual('./app_links');
jest.mock('../../app_links', () => {
const actual = jest.requireActual('../../app_links');
const fakeLink = {
id: fakePageId,
title: 'test fake menu item',

View file

@ -5,25 +5,28 @@
* 2.0.
*/
import type { CoreStart } from '@kbn/core/public';
import useObservable from 'react-use/lib/useObservable';
import { map } from 'rxjs';
import type { Subscription } from 'rxjs';
import { BehaviorSubject, map } from 'rxjs';
import { appLinks$ } from './links';
import type { SecurityPageName } from '../../app/types';
import type { AppLinkItems, NavigationLink } from './types';
import type { SecurityNavLink, AppLinkItems, NavigationLink } from './types';
export const formatNavigationLinks = (appLinks: AppLinkItems): NavigationLink[] =>
appLinks.map<NavigationLink>((link) => ({
export const formatNavigationLinks = (appLinks: AppLinkItems): SecurityNavLink[] =>
appLinks.map<SecurityNavLink>((link) => ({
id: link.id,
title: link.title,
...(link.categories != null ? { categories: link.categories } : {}),
...(link.description != null ? { description: link.description } : {}),
...(link.sideNavDisabled === true ? { disabled: true } : {}),
...(link.landingIcon != null ? { landingIcon: link.landingIcon } : {}),
...(link.landingImage != null ? { landingImage: link.landingImage } : {}),
...(link.sideNavIcon != null ? { sideNavIcon: link.sideNavIcon } : {}),
...(link.skipUrlState != null ? { skipUrlState: link.skipUrlState } : {}),
...(link.isBeta != null ? { isBeta: link.isBeta } : {}),
...(link.betaOptions != null ? { betaOptions: link.betaOptions } : {}),
...(link.categories != null && { categories: link.categories }),
...(link.description != null && { description: link.description }),
...(link.sideNavDisabled === true && { disabled: true }),
...(link.landingIcon != null && { landingIcon: link.landingIcon }),
...(link.landingImage != null && { landingImage: link.landingImage }),
...(link.sideNavIcon != null && { sideNavIcon: link.sideNavIcon }),
...(link.sideNavFooter != null && { isFooterLink: link.sideNavFooter }),
...(link.skipUrlState != null && { skipUrlState: link.skipUrlState }),
...(link.isBeta != null && { isBeta: link.isBeta }),
...(link.betaOptions != null && { betaOptions: link.betaOptions }),
...(link.links?.length && {
links: formatNavigationLinks(link.links),
}),
@ -33,12 +36,46 @@ export const formatNavigationLinks = (appLinks: AppLinkItems): NavigationLink[]
* Navigation links observable based on Security AppLinks,
* It is used to generate the side navigation items
*/
export const navLinks$ = appLinks$.pipe(map(formatNavigationLinks));
export const internalNavLinks$ = appLinks$.pipe(map(formatNavigationLinks));
export const navLinksUpdater$ = new BehaviorSubject<NavigationLink[]>([]);
export const navLinks$ = navLinksUpdater$.asObservable();
let currentSubscription: Subscription;
export const updateNavLinks = (isSolutionNavEnabled: boolean, core: CoreStart) => {
if (currentSubscription) {
currentSubscription.unsubscribe();
}
if (isSolutionNavEnabled) {
// import solution nav links only when solution nav is enabled
lazySolutionNavLinks().then((createSolutionNavLinks$) => {
currentSubscription = createSolutionNavLinks$(internalNavLinks$, core).subscribe((links) => {
navLinksUpdater$.next(links);
});
});
} else {
currentSubscription = internalNavLinks$.subscribe((links) => {
navLinksUpdater$.next(links);
});
}
};
// includes internal security links only
export const useSecurityInternalNavLinks = (): SecurityNavLink[] => {
return useObservable(internalNavLinks$, []);
};
// includes internal security links and externals links to other applications such as discover, ml, etc.
export const useNavLinks = (): NavigationLink[] => {
return useObservable(navLinks$, []);
return useObservable(navLinks$, navLinksUpdater$.value); // use default value from updater subject to prevent re-renderings
};
export const useRootNavLink = (linkId: SecurityPageName): NavigationLink | undefined => {
return useNavLinks().find(({ id }) => id === linkId);
};
const lazySolutionNavLinks = async () =>
import(
/* webpackChunkName: "solution_nav_links" */
'../../app/solution_navigation/links/nav_links'
).then(({ createSolutionNavLinks$ }) => createSolutionNavLinks$);

View file

@ -9,16 +9,28 @@ import type { Capabilities } from '@kbn/core/types';
import type { ILicense, LicenseType } from '@kbn/licensing-plugin/common/types';
import type { IconType } from '@elastic/eui';
import type {
SecurityPageName,
NavigationLink as GenericNavigationLink,
LinkCategory as GenericLinkCategory,
LinkCategories as GenericLinkCategories,
ExternalPageName,
SecurityPageName,
} from '@kbn/security-solution-navigation';
import type { UpsellingService } from '@kbn/security-solution-upselling/service';
import type { AppDeepLinkLocations } from '@kbn/core-application-browser';
import type { Observable } from 'rxjs';
import type { SolutionSideNavItem as ClassicSolutionSideNavItem } from '@kbn/security-solution-side-nav';
import type { ExperimentalFeatures } from '../../../common/experimental_features';
import type { RequiredCapabilities } from '../lib/capabilities';
export type SecurityNavLink = GenericNavigationLink<SecurityPageName>;
export type SolutionPageName = SecurityPageName | ExternalPageName;
export type SolutionNavLink = GenericNavigationLink<SolutionPageName>;
export type SolutionNavLinks$ = Observable<SolutionNavLink[]>;
export type SolutionLinkCategory = GenericLinkCategory<SolutionPageName>;
export type SolutionSideNavItem = ClassicSolutionSideNavItem<SolutionPageName>;
/**
* Permissions related parameters needed for the links to be filtered
*/
@ -47,7 +59,7 @@ export interface LinkItem {
/**
* Categories to display in the navigation
*/
categories?: LinkCategories;
categories?: GenericLinkCategories<SecurityPageName>;
/**
* The description of the link content
*/
@ -114,6 +126,10 @@ export interface LinkItem {
* Link path relative to security root
*/
path: string;
/**
* Displays the link in the footer of the side navigation. Defaults to false.
*/
sideNavFooter?: boolean;
/**
* Disables link in the side navigation. Defaults to false.
*/
@ -141,12 +157,11 @@ export interface LinkItem {
}
export type AppLinkItems = Readonly<LinkItem[]>;
export type AppLinksSwitcher = (appLinks: AppLinkItems) => AppLinkItems;
export type LinkInfo = Omit<LinkItem, 'links'>;
export type NormalizedLink = LinkInfo & { parentId?: SecurityPageName };
export type NormalizedLinks = Partial<Record<SecurityPageName, NormalizedLink>>;
export type NavigationLink = GenericNavigationLink<SecurityPageName>;
export type LinkCategory = GenericLinkCategory<SecurityPageName>;
export type LinkCategories = GenericLinkCategories<SecurityPageName>;
export type NavigationLink = GenericNavigationLink<SolutionPageName>;
export type LinkCategory = GenericLinkCategory<SolutionPageName>;
export type LinkCategories = GenericLinkCategories<SolutionPageName>;

View file

@ -38,7 +38,7 @@ import { SUB_PLUGINS_REDUCER, mockGlobalState, createMockStore } from '..';
import type { ExperimentalFeatures } from '../../../../common/experimental_features';
import { APP_UI_ID, APP_PATH } from '../../../../common/constants';
import { KibanaServices } from '../../lib/kibana';
import { links } from '../../links/app_links';
import { appLinks } from '../../../app_links';
import { fleetGetPackageHttpMock } from '../../../management/mocks';
import { allowedExperimentalValues } from '../../../../common/experimental_features';
@ -229,8 +229,10 @@ export const createAppRootMockRenderer = (): AppContextTestRender => {
// hide react-query output in console
logger: {
error: () => {},
// eslint-disable-next-line no-console
log: console.log,
// eslint-disable-next-line no-console
warn: console.warn,
},
@ -343,7 +345,7 @@ const createCoreStartMock = (
): ReturnType<typeof coreMock.createStart> => {
const coreStart = coreMock.createStart({ basePath: '/mock' });
const linkPaths = getLinksPaths(links);
const linkPaths = getLinksPaths(appLinks);
// Mock the certain APP Ids returned by `application.getUrlForApp()`
coreStart.application.getUrlForApp.mockImplementation((appId, { deepLinkId, path } = {}) => {
@ -376,8 +378,8 @@ const createCoreStartMock = (
return coreStart;
};
const getLinksPaths = (appLinks: AppLinkItems): Record<string, string> => {
return appLinks.reduce((result: Record<string, string>, link) => {
const getLinksPaths = (links: AppLinkItems): Record<string, string> => {
return links.reduce((result: Record<string, string>, link) => {
if (link.path) {
result[link.id] = link.path;
}

View file

@ -9,7 +9,7 @@ import { renderHook, act } from '@testing-library/react-hooks';
import { allowedExperimentalValues } from '../../../../common/experimental_features';
import { UpsellingService } from '@kbn/security-solution-upselling/service';
import { updateAppLinks } from '../../links';
import { links } from '../../links/app_links';
import { appLinks } from '../../../app_links';
import { useShowTimeline } from './use_show_timeline';
const mockUseLocation = jest.fn().mockReturnValue({ pathname: '/overview' });
@ -57,7 +57,7 @@ const mockUpselling = new UpsellingService();
describe('use show timeline', () => {
beforeAll(() => {
// initialize all App links before running test
updateAppLinks(links, {
updateAppLinks(appLinks, {
experimentalFeatures: allowedExperimentalValues,
capabilities: {
navLinks: {},

View file

@ -24,6 +24,7 @@ import {
readCasesCapabilities,
} from './cases_test_utils';
import { createStartServicesMock } from './common/lib/kibana/kibana_react.mock';
import { set } from '@kbn/safer-lodash-set';
const mockServices = createStartServicesMock();
@ -85,12 +86,12 @@ describe('#getSubPluginRoutesByCapabilities', () => {
it('cases routes should return NoPrivilegesPage component when cases plugin is NOT available ', () => {
const routes = getSubPluginRoutesByCapabilities(
mockSubPlugins,
{
set(mockServices, 'application.capabilities', {
[SERVER_APP_ID]: { show: true, crud: false },
[CASES_FEATURE_ID]: noCasesCapabilities(),
} as unknown as Capabilities,
mockServices
})
);
const casesRoute = routes.find((r) => r.path === 'cases');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const CasesView = (casesRoute?.component ?? mockRender) as React.ComponentType<any>;
@ -105,11 +106,10 @@ describe('#getSubPluginRoutesByCapabilities', () => {
it('alerts should return NoPrivilegesPage component when siem plugin is NOT available ', () => {
const routes = getSubPluginRoutesByCapabilities(
mockSubPlugins,
{
set(mockServices, 'application.capabilities', {
[SERVER_APP_ID]: { show: false, crud: false },
[CASES_FEATURE_ID]: readCasesCapabilities(),
} as unknown as Capabilities,
mockServices
})
);
const alertsRoute = routes.find((r) => r.path === 'alerts');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -125,11 +125,10 @@ describe('#getSubPluginRoutesByCapabilities', () => {
it('should return NoPrivilegesPage for each route when both plugins are NOT available ', () => {
const routes = getSubPluginRoutesByCapabilities(
mockSubPlugins,
{
set(mockServices, 'application.capabilities', {
[SERVER_APP_ID]: { show: false, crud: false },
[CASES_FEATURE_ID]: noCasesCapabilities(),
} as unknown as Capabilities,
mockServices
})
);
const casesRoute = routes.find((r) => r.path === 'cases');
// eslint-disable-next-line @typescript-eslint/no-explicit-any

View file

@ -206,11 +206,10 @@ export const isThreatIntelligencePath = (pathname: string): boolean => {
export const getSubPluginRoutesByCapabilities = (
subPlugins: StartedSubPlugins,
capabilities: Capabilities,
services: StartServices
): RouteProps[] => {
return Object.entries(subPlugins).reduce<RouteProps[]>((acc, [key, value]) => {
if (isSubPluginAvailable(key, capabilities)) {
if (isSubPluginAvailable(key, services.application.capabilities)) {
acc.push(...value.routes);
} else {
const docLinkSelector = (docLinks: DocLinks) => docLinks.siem.privileges;

View file

@ -5,5 +5,15 @@
* 2.0.
*/
export const useNavLinks = jest.fn(() => []);
export const useNavLink = jest.fn(() => undefined);
import type { SecuritySubPlugin } from '../app/types';
import { routes } from './routes';
export class Investigations {
public setup() {}
public start(): SecuritySubPlugin {
return {
routes,
};
}
}

View file

@ -10,26 +10,20 @@ import { LandingLinksIcons } from '@kbn/security-solution-navigation/landing_lin
import { SecurityPageName } from '@kbn/security-solution-navigation';
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
import { EuiPageHeader, EuiSpacer } from '@elastic/eui';
import { TrackApplicationView } from '@kbn/usage-collection-plugin/public';
import { useNavLink } from '../common/hooks/use_nav_links';
import { useRootNavLink } from '../common/links/nav_links';
export const InvestigationsRoute: React.FC = () => {
const link = useNavLink(SecurityPageName.investigations);
export const Investigations: React.FC = () => {
const link = useRootNavLink(SecurityPageName.investigations);
const { links = [], title } = link ?? {};
return (
<KibanaPageTemplate restrictWidth={false} contentBorder={false} grow={true}>
<KibanaPageTemplate.Section>
<TrackApplicationView viewId={SecurityPageName.investigations}>
<EuiPageHeader pageTitle={title} />
<EuiSpacer size="l" />
<EuiSpacer size="xl" />
<LandingLinksIcons items={links} />
</TrackApplicationView>
<EuiPageHeader pageTitle={title} />
<EuiSpacer size="l" />
<EuiSpacer size="xl" />
<LandingLinksIcons items={links} />
</KibanaPageTemplate.Section>
</KibanaPageTemplate>
);
};
// eslint-disable-next-line import/no-default-export
export default InvestigationsRoute;

View file

@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { SecurityPageName } from '@kbn/security-solution-navigation';
import { INVESTIGATIONS_PATH } from '../../common/constants';
import type { SecuritySubPluginRoutes } from '../app/types';
import { Investigations } from './investigations';
import { PluginTemplateWrapper } from '../common/components/plugin_template_wrapper';
import { SecurityRoutePageWrapper } from '../common/components/security_route_page_wrapper';
const InvestigationsPage = React.memo(function InvestigationsPage() {
return (
<PluginTemplateWrapper>
<SecurityRoutePageWrapper pageName={SecurityPageName.investigations} redirectOnMissing>
<Investigations />
</SecurityRoutePageWrapper>
</PluginTemplateWrapper>
);
});
export const routes: SecuritySubPluginRoutes = [
{
path: INVESTIGATIONS_PATH,
component: InvestigationsPage,
},
];

View file

@ -24,6 +24,9 @@ import { CloudSecurityPosture } from './cloud_security_posture';
import { ThreatIntelligence } from './threat_intelligence';
import { Dashboards } from './dashboards';
import { EntityAnalytics } from './entity_analytics';
import { Assets } from './assets';
import { Investigations } from './investigations';
import { MachineLearning } from './machine_learning';
/**
* The classes used to instantiate the sub plugins. These are grouped into a single object for the sake of bundling them in a single dynamic import.
@ -43,5 +46,8 @@ const subPluginClasses = {
CloudSecurityPosture,
ThreatIntelligence,
EntityAnalytics,
Assets,
Investigations,
MachineLearning,
};
export { subPluginClasses };

View file

@ -0,0 +1,19 @@
/*
* 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 { SecuritySubPlugin } from '../app/types';
import { routes } from './routes';
export class MachineLearning {
public setup() {}
public start(): SecuritySubPlugin {
return {
routes,
};
}
}

View file

@ -10,26 +10,20 @@ import { LandingLinksIconsCategories } from '@kbn/security-solution-navigation/l
import { SecurityPageName } from '@kbn/security-solution-navigation';
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
import { EuiPageHeader, EuiSpacer } from '@elastic/eui';
import { TrackApplicationView } from '@kbn/usage-collection-plugin/public';
import { useNavLink } from '../common/hooks/use_nav_links';
import { useRootNavLink } from '../common/links/nav_links';
export const MachineLearningRoute: React.FC = () => {
const link = useNavLink(SecurityPageName.mlLanding);
export const MachineLearning: React.FC = () => {
const link = useRootNavLink(SecurityPageName.mlLanding);
const { links = [], categories = [], title } = link ?? {};
return (
<KibanaPageTemplate restrictWidth={false} contentBorder={false} grow={true}>
<KibanaPageTemplate.Section>
<TrackApplicationView viewId={SecurityPageName.mlLanding}>
<EuiPageHeader pageTitle={title} />
<EuiSpacer size="l" />
<EuiSpacer size="xl" />
<LandingLinksIconsCategories links={links} categories={categories} />
</TrackApplicationView>
<EuiPageHeader pageTitle={title} />
<EuiSpacer size="l" />
<EuiSpacer size="xl" />
<LandingLinksIconsCategories links={links} categories={categories} />
</KibanaPageTemplate.Section>
</KibanaPageTemplate>
);
};
// eslint-disable-next-line import/no-default-export
export default MachineLearningRoute;

View file

@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { SecurityPageName } from '@kbn/security-solution-navigation';
import { MACHINE_LEARNING_PATH } from '../../common/constants';
import type { SecuritySubPluginRoutes } from '../app/types';
import { MachineLearning } from './machine_learning';
import { PluginTemplateWrapper } from '../common/components/plugin_template_wrapper';
import { SecurityRoutePageWrapper } from '../common/components/security_route_page_wrapper';
const MachineLearningPage = React.memo(function MachineLearningPage() {
return (
<PluginTemplateWrapper>
<SecurityRoutePageWrapper pageName={SecurityPageName.mlLanding} redirectOnMissing>
<MachineLearning />
</SecurityRoutePageWrapper>
</PluginTemplateWrapper>
);
});
export const routes: SecuritySubPluginRoutes = [
{
path: MACHINE_LEARNING_PATH,
component: MachineLearningPage,
},
];

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import { TrackApplicationView } from '@kbn/usage-collection-plugin/public';
import { SecurityRoutePageWrapper } from '../common/components/security_route_page_wrapper';
import { MANAGEMENT_PATH, MANAGE_PATH } from '../../common/constants';
import { SecurityPageName } from '../app/types';
import { ManagementContainer } from './pages';
@ -17,9 +17,9 @@ import { ManageLandingPage } from './pages/landing';
const ManagementLanding = () => (
<PluginTemplateWrapper>
<TrackApplicationView viewId={SecurityPageName.administration}>
<SecurityRoutePageWrapper pageName={SecurityPageName.administration} redirectOnMissing>
<ManageLandingPage />
</TrackApplicationView>
</SecurityRoutePageWrapper>
</PluginTemplateWrapper>
);

View file

@ -17,7 +17,6 @@ const upselling = new UpsellingService();
const onboardingPageService = new OnboardingPageService();
export const contractStartServicesMock: ContractStartServices = {
extraRoutes$: of([]),
getComponents$: jest.fn(() => of({})),
upselling,
onboarding: onboardingPageService,
@ -26,8 +25,6 @@ export const contractStartServicesMock: ContractStartServices = {
const setupMock = (): PluginSetup => ({
resolver: jest.fn(),
experimentalFeatures: allowedExperimentalValues, // default values
setAppLinksSwitcher: jest.fn(),
setDeepLinksFormatter: jest.fn(),
});
const startMock = (): PluginStart => ({
@ -36,9 +33,13 @@ const startMock = (): PluginStart => ({
getBreadcrumbsNav$: jest.fn(
() => new BehaviorSubject<BreadcrumbsNav>({ leading: [], trailing: [] })
),
setExtraRoutes: jest.fn(),
getUpselling: () => upselling,
setOnboardingPageSettings: onboardingPageService,
setIsSolutionNavigationEnabled: jest.fn(),
getSolutionNavigation: jest.fn(async () => ({
navigationTree$: of({ body: [], footer: [] }),
panelContentProvider: jest.fn(),
})),
});
export const securitySolutionMock = {

View file

@ -56,6 +56,7 @@ export const gettingStartedLinks: LinkItem = {
}),
],
sideNavIcon: 'launch',
sideNavFooter: true,
skipUrlState: true,
hideTimeline: true,
};

View file

@ -6,7 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
import { Subject, mergeMap, firstValueFrom } from 'rxjs';
import { Subject, combineLatestWith } from 'rxjs';
import type * as H from 'history';
import type {
AppMountParameters,
@ -15,16 +15,11 @@ import type {
CoreStart,
PluginInitializerContext,
Plugin as IPlugin,
AppMount,
} from '@kbn/core/public';
import {
type DataPublicPluginStart,
FilterManager,
NowProvider,
QueryService,
} from '@kbn/data-plugin/public';
import { DEFAULT_APP_CATEGORIES } from '@kbn/core/public';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import type { TriggersAndActionsUIPublicPluginSetup } from '@kbn/triggers-actions-ui-plugin/public';
import { getLazyEndpointAgentTamperProtectionExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_agent_tamper_protection_extension';
import type { FleetUiExtensionGetterOptions } from './management/pages/policy/view/ingest_manager_integration/types';
import type {
@ -37,17 +32,14 @@ import type {
StartedSubPlugins,
StartPluginsDependencies,
} from './types';
import { initTelemetry, TelemetryService } from './common/lib/telemetry';
import { KibanaServices } from './common/lib/kibana/services';
import { SOLUTION_NAME } from './common/translations';
import { APP_ID, APP_UI_ID, APP_PATH, APP_ICON_SOLUTION } from '../common/constants';
import type { AppLinkItems } from './common/links';
import { updateAppLinks, type LinksPermissions } from './common/links';
import { registerDeepLinksUpdater } from './common/links/deep_links';
import { licenseService } from './common/hooks/use_license';
import type { SecuritySolutionUiConfigType } from './common/types';
import { ExperimentalFeaturesService } from './common/experimental_features_service';
import { getLazyEndpointPolicyEditExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension';
import { getLazyEndpointPolicyCreateExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension';
@ -62,100 +54,49 @@ import { LazyCustomCriblExtension } from './security_integrations/cribl/componen
import type { SecurityAppStore } from './common/store/types';
import { PluginContract } from './plugin_contract';
import { TopValuesPopoverService } from './app/components/top_values_popover/top_values_popover_service';
import { parseConfigSettings, type ConfigSettings } from '../common/config_settings';
import { PluginServices } from './plugin_services';
import { getExternalReferenceAttachmentEndpointRegular } from './cases/attachments/external_reference';
export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, StartPlugins> {
/**
* The current Kibana branch. e.g. 'main'
*/
readonly kibanaBranch: string;
/**
* The current Kibana version. e.g. '8.0.0' or '8.0.0-SNAPSHOT'
*/
readonly kibanaVersion: string;
/**
* Whether the environment is 'serverless' or 'traditional'
*/
readonly buildFlavor: string;
/**
* For internal use. Specify which version of the Detection Rules fleet package to install
* when upgrading rules. If not provided, the latest compatible package will be installed,
* or if running from a dev environment or -SNAPSHOT build, the latest pre-release package
* will be used (if fleet is available or not within an airgapped environment).
*
* Note: This is for `upgrade only`, which occurs by means of the `useUpgradeSecurityPackages`
* hook when navigating to a Security Solution page. The package version specified in
* `fleet_packages.json` in project root will always be installed first on Kibana start if
* the package is not already installed.
*/
readonly prebuiltRulesPackageVersion?: string;
private config: SecuritySolutionUiConfigType;
private experimentalFeatures: ExperimentalFeatures;
private contract: PluginContract;
private telemetry: TelemetryService;
private services: PluginServices;
readonly experimentalFeatures: ExperimentalFeatures;
readonly configSettings: ConfigSettings;
private queryService: QueryService = new QueryService();
private nowProvider: NowProvider = new NowProvider();
private appUpdater$ = new Subject<AppUpdater>();
private storage = new Storage(localStorage);
// Lazily instantiated dependencies
private _subPlugins?: SubPlugins;
private _store?: SecurityAppStore;
private _actionsRegistered?: boolean = false;
private _alertsTableRegistered?: boolean = false;
constructor(private readonly initializerContext: PluginInitializerContext) {
this.config = this.initializerContext.config.get<SecuritySolutionUiConfigType>();
this.experimentalFeatures = parseExperimentalConfigValue(
this.config.enableExperimental || []
).features;
this.configSettings = parseConfigSettings(this.config.offeringSettings ?? {}).settings;
this.kibanaVersion = initializerContext.env.packageInfo.version;
this.kibanaBranch = initializerContext.env.packageInfo.branch;
this.buildFlavor = initializerContext.env.packageInfo.buildFlavor;
this.prebuiltRulesPackageVersion = this.config.prebuiltRulesPackageVersion;
this.contract = new PluginContract(this.experimentalFeatures);
this.telemetry = new TelemetryService();
this.storage = new Storage(window.localStorage);
this.services = new PluginServices(
this.config,
this.experimentalFeatures,
this.contract,
initializerContext.env.packageInfo
);
}
private appUpdater$ = new Subject<AppUpdater>();
private storage = new Storage(localStorage);
private sessionStorage = new Storage(sessionStorage);
/**
* Lazily instantiated subPlugins.
* See `subPlugins` method.
*/
private _subPlugins?: SubPlugins;
/**
* Lazily instantiated `SecurityAppStore`.
* See `store` method.
*/
private _store?: SecurityAppStore;
private _actionsRegistered?: boolean = false;
public setup(
core: CoreSetup<StartPluginsDependencies, PluginStart>,
plugins: SetupPlugins
): PluginSetup {
initTelemetry(
{
usageCollection: plugins.usageCollection,
},
APP_UI_ID
);
const telemetryContext = {
prebuiltRulesPackageVersion: this.prebuiltRulesPackageVersion,
};
this.telemetry.setup({ analytics: core.analytics }, telemetryContext);
this.services.setup(core, plugins);
this.queryService?.setup({
uiSettings: core.uiSettings,
storage: this.storage,
nowProvider: this.nowProvider,
});
const { home, triggersActionsUi, usageCollection } = plugins;
if (plugins.home) {
plugins.home.featureCatalogue.registerSolution({
if (home) {
home.featureCatalogue.registerSolution({
id: APP_ID,
title: SOLUTION_NAME,
description: i18n.translate('xpack.securitySolution.featureCatalogueDescription', {
@ -168,64 +109,25 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
});
}
/**
* `StartServices` which are needed by the `renderApp` function when mounting any of the subPlugin applications.
* This is a promise because these aren't available until the `start` lifecycle phase but they are referenced
* in the `setup` lifecycle phase.
*/
const startServices = async (params: AppMountParameters<unknown>): Promise<StartServices> => {
const [coreStart, startPluginsDeps] = await core.getStartServices();
const { apm } = await import('@elastic/apm-rum');
const { SecuritySolutionTemplateWrapper } = await import('./app/home/template_wrapper');
const mount: AppMount = async (params) => {
const [coreStart, startPlugins] = await core.getStartServices();
const services = await this.services.generateServices(coreStart, startPlugins, params);
const { savedObjectsTaggingOss, ...startPlugins } = startPluginsDeps;
const subPlugins = await this.startSubPlugins(this.storage, coreStart, startPlugins);
const store = await this.store(coreStart, startPlugins, subPlugins);
const query = this.queryService.start({
uiSettings: core.uiSettings,
storage: this.storage,
http: core.http,
});
const { renderApp } = await this.lazyApplicationDependencies();
const { getSubPluginRoutesByCapabilities } = await this.lazyHelpersForRoutes();
// used for creating a custom stateful KQL Query Bar
const customDataService: DataPublicPluginStart = {
...startPlugins.data,
query,
// @ts-expect-error
_name: 'custom',
};
await this.registerActions(store, params.history, services);
await this.registerAlertsTableConfiguration(triggersActionsUi);
// @ts-expect-error
customDataService.query.filterManager._name = 'customFilterManager';
const subPluginRoutes = getSubPluginRoutesByCapabilities(subPlugins, services);
const sideNavEnabled = await this.getIsSidebarEnabled(core);
const services: StartServices = {
...coreStart,
...startPlugins,
...this.contract.getStartServices(),
configSettings: {
...this.configSettings,
sideNavEnabled,
},
apm,
savedObjectsTagging: savedObjectsTaggingOss.getTaggingApi(),
setHeaderActionMenu: params.setHeaderActionMenu,
storage: this.storage,
sessionStorage: this.sessionStorage,
security: startPluginsDeps.security,
onAppLeave: params.onAppLeave,
securityLayout: {
getPluginWrapper: () => SecuritySolutionTemplateWrapper,
},
contentManagement: startPluginsDeps.contentManagement,
telemetry: this.telemetry.start(),
customDataService,
topValuesPopover: new TopValuesPopoverService(),
timelineFilterManager: new FilterManager(coreStart.uiSettings),
};
return services;
return renderApp({ ...params, services, store, usageCollection, subPluginRoutes });
};
// Register main Security Solution plugin
core.application.register({
id: APP_UI_ID,
title: SOLUTION_NAME,
@ -234,36 +136,10 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
updater$: this.appUpdater$,
visibleIn: ['globalSearch', 'home', 'kibanaOverview'],
euiIconType: APP_ICON_SOLUTION,
mount: async (params: AppMountParameters) => {
// required to show the alert table inside cases
const { alertsTableConfigurationRegistry } = plugins.triggersActionsUi;
const { registerAlertsTableConfiguration } =
await this.lazyRegisterAlertsTableConfiguration();
registerAlertsTableConfiguration(alertsTableConfigurationRegistry, this.storage);
const [coreStart, startPlugins] = await core.getStartServices();
const subPlugins = await this.startSubPlugins(this.storage, coreStart, startPlugins);
const store = await this.store(coreStart, startPlugins, subPlugins);
const services = await startServices(params);
await this.registerActions(store, params.history, services);
const { renderApp } = await this.lazyApplicationDependencies();
const { getSubPluginRoutesByCapabilities } = await this.lazyHelpersForRoutes();
return renderApp({
...params,
services,
store,
usageCollection: plugins.usageCollection,
subPluginRoutes: getSubPluginRoutesByCapabilities(
subPlugins,
coreStart.application.capabilities,
services
),
});
},
mount,
});
// Register legacy SIEM app for backward compatibility
core.application.register({
id: 'siem',
appRoute: 'app/siem',
@ -291,16 +167,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
}
public start(core: CoreStart, plugins: StartPlugins): PluginStart {
KibanaServices.init({
...core,
...plugins,
kibanaBranch: this.kibanaBranch,
kibanaVersion: this.kibanaVersion,
buildFlavor: this.buildFlavor,
prebuiltRulesPackageVersion: this.prebuiltRulesPackageVersion,
});
ExperimentalFeaturesService.init({ experimentalFeatures: this.experimentalFeatures });
licenseService.start(plugins.licensing.license$);
this.services.start(core, plugins);
if (plugins.fleet) {
const { registerExtension } = plugins.fleet;
@ -370,15 +237,161 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
// Not using await to prevent blocking start execution
this.registerAppLinks(core, plugins);
return this.contract.getStartContract();
return this.contract.getStartContract(core);
}
public stop() {
this.queryService.stop();
licenseService.stop();
this.contract.getStopContract();
this.services.stop();
}
private async createSubPlugins(): Promise<SubPlugins> {
if (!this._subPlugins) {
const { subPluginClasses } = await this.lazySubPlugins();
this._subPlugins = {
alerts: new subPluginClasses.Detections(),
rules: new subPluginClasses.Rules(),
exceptions: new subPluginClasses.Exceptions(),
cases: new subPluginClasses.Cases(),
dashboards: new subPluginClasses.Dashboards(),
explore: new subPluginClasses.Explore(),
kubernetes: new subPluginClasses.Kubernetes(),
overview: new subPluginClasses.Overview(),
timelines: new subPluginClasses.Timelines(),
management: new subPluginClasses.Management(),
cloudDefend: new subPluginClasses.CloudDefend(),
cloudSecurityPosture: new subPluginClasses.CloudSecurityPosture(),
threatIntelligence: new subPluginClasses.ThreatIntelligence(),
entityAnalytics: new subPluginClasses.EntityAnalytics(),
assets: new subPluginClasses.Assets(),
investigations: new subPluginClasses.Investigations(),
machineLearning: new subPluginClasses.MachineLearning(),
};
}
return this._subPlugins;
}
/**
* All started subPlugins.
*/
private async startSubPlugins(
storage: Storage,
core: CoreStart,
plugins: StartPlugins
): Promise<StartedSubPlugins> {
const subPlugins = await this.createSubPlugins();
return {
alerts: subPlugins.alerts.start(storage),
cases: subPlugins.cases.start(),
cloudDefend: subPlugins.cloudDefend.start(),
cloudSecurityPosture: subPlugins.cloudSecurityPosture.start(),
dashboards: subPlugins.dashboards.start(),
exceptions: subPlugins.exceptions.start(storage),
explore: subPlugins.explore.start(storage),
kubernetes: subPlugins.kubernetes.start(),
management: subPlugins.management.start(core, plugins),
overview: subPlugins.overview.start(),
rules: subPlugins.rules.start(storage),
threatIntelligence: subPlugins.threatIntelligence.start(),
timelines: subPlugins.timelines.start(),
entityAnalytics: subPlugins.entityAnalytics.start(
this.experimentalFeatures.riskScoringRoutesEnabled
),
assets: subPlugins.assets.start(),
investigations: subPlugins.investigations.start(),
machineLearning: subPlugins.machineLearning.start(),
};
}
/**
* Lazily instantiate a `SecurityAppStore`. We lazily instantiate this because it requests large dynamic imports. We instantiate it once because each subPlugin needs to share the same reference.
*/
private async store(
coreStart: CoreStart,
startPlugins: StartPlugins,
subPlugins: StartedSubPlugins
): Promise<SecurityAppStore> {
if (!this._store) {
const { createStoreFactory } = await this.lazyApplicationDependencies();
this._store = await createStoreFactory(
coreStart,
startPlugins,
subPlugins,
this.storage,
this.experimentalFeatures
);
}
if (startPlugins.timelines) {
startPlugins.timelines.setTimelineEmbeddedStore(this._store);
}
return this._store;
}
private async registerActions(
store: SecurityAppStore,
history: H.History,
services: StartServices
) {
if (!this._actionsRegistered) {
const { registerActions } = await this.lazyActions();
registerActions(store, history, services);
this._actionsRegistered = true;
}
}
/**
* Registers the alerts tables configurations.
*/
private async registerAlertsTableConfiguration(
triggersActionsUi: TriggersAndActionsUIPublicPluginSetup
) {
if (!this._alertsTableRegistered) {
const { registerAlertsTableConfiguration } =
await this.lazyRegisterAlertsTableConfiguration();
registerAlertsTableConfiguration(
triggersActionsUi.alertsTableConfigurationRegistry,
this.storage
);
this._alertsTableRegistered = true;
}
}
/**
* Registers deepLinks and appUpdater for appLinks using license.
*/
async registerAppLinks(core: CoreStart, plugins: StartPlugins) {
const {
appLinks: initialAppLinks,
getFilteredLinks,
solutionAppLinksSwitcher,
} = await this.lazyApplicationLinks();
const { license$ } = plugins.licensing;
const { upsellingService, isSolutionNavigationEnabled$ } = this.contract;
registerDeepLinksUpdater(this.appUpdater$, isSolutionNavigationEnabled$);
const appLinks$ = new Subject<AppLinkItems>();
appLinks$.next(initialAppLinks);
appLinks$
.pipe(combineLatestWith(license$, isSolutionNavigationEnabled$))
.subscribe(([appLinks, license, isSolutionNavigationEnabled]) => {
const links = isSolutionNavigationEnabled ? solutionAppLinksSwitcher(appLinks) : appLinks;
const linksPermissions: LinksPermissions = {
experimentalFeatures: this.experimentalFeatures,
upselling: upsellingService,
capabilities: core.application.capabilities,
...(license.type != null && { license }),
};
updateAppLinks(links, linksPermissions);
});
const filteredLinks = await getFilteredLinks(core, plugins);
appLinks$.next(filteredLinks);
}
// Lazy loaded dependencies
private lazyHelpersForRoutes() {
/**
* The specially formatted comment in the `import` expression causes the corresponding webpack chunk to be named. This aids us in debugging chunk size issues.
@ -438,7 +451,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
*/
return import(
/* webpackChunkName: "lazy_app_links" */
'./common/links/app_links'
'./app_links'
);
}
@ -452,137 +465,4 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
'./actions'
);
}
/**
* Lazily instantiated subPlugins. This should be instantiated just once.
*/
private async subPlugins(): Promise<SubPlugins> {
if (!this._subPlugins) {
const { subPluginClasses } = await this.lazySubPlugins();
this._subPlugins = {
alerts: new subPluginClasses.Detections(),
rules: new subPluginClasses.Rules(),
exceptions: new subPluginClasses.Exceptions(),
cases: new subPluginClasses.Cases(),
dashboards: new subPluginClasses.Dashboards(),
explore: new subPluginClasses.Explore(),
kubernetes: new subPluginClasses.Kubernetes(),
overview: new subPluginClasses.Overview(),
timelines: new subPluginClasses.Timelines(),
management: new subPluginClasses.Management(),
cloudDefend: new subPluginClasses.CloudDefend(),
cloudSecurityPosture: new subPluginClasses.CloudSecurityPosture(),
threatIntelligence: new subPluginClasses.ThreatIntelligence(),
entityAnalytics: new subPluginClasses.EntityAnalytics(),
};
}
return this._subPlugins;
}
/**
* All started subPlugins.
*/
private async startSubPlugins(
storage: Storage,
core: CoreStart,
plugins: StartPlugins
): Promise<StartedSubPlugins> {
const subPlugins = await this.subPlugins();
return {
alerts: subPlugins.alerts.start(storage),
cases: subPlugins.cases.start(),
cloudDefend: subPlugins.cloudDefend.start(),
cloudSecurityPosture: subPlugins.cloudSecurityPosture.start(),
dashboards: subPlugins.dashboards.start(),
exceptions: subPlugins.exceptions.start(storage),
explore: subPlugins.explore.start(storage),
kubernetes: subPlugins.kubernetes.start(),
management: subPlugins.management.start(core, plugins),
overview: subPlugins.overview.start(),
rules: subPlugins.rules.start(storage),
threatIntelligence: subPlugins.threatIntelligence.start(),
timelines: subPlugins.timelines.start(),
entityAnalytics: subPlugins.entityAnalytics.start(
this.experimentalFeatures.riskScoringRoutesEnabled
),
};
}
/**
* Lazily instantiate a `SecurityAppStore`. We lazily instantiate this because it requests large dynamic imports. We instantiate it once because each subPlugin needs to share the same reference.
*/
private async store(
coreStart: CoreStart,
startPlugins: StartPlugins,
subPlugins: StartedSubPlugins
): Promise<SecurityAppStore> {
if (!this._store) {
const { createStoreFactory } = await this.lazyApplicationDependencies();
this._store = await createStoreFactory(
coreStart,
startPlugins,
subPlugins,
this.storage,
this.experimentalFeatures
);
}
if (startPlugins.timelines) {
startPlugins.timelines.setTimelineEmbeddedStore(this._store);
}
return this._store;
}
private async registerActions(
store: SecurityAppStore,
history: H.History,
services: StartServices
) {
if (!this._actionsRegistered) {
const { registerActions } = await this.lazyActions();
registerActions(store, history, services);
this._actionsRegistered = true;
}
}
/**
* Register deepLinks and appUpdater for all app links, to change deepLinks as needed when licensing changes.
*/
async registerAppLinks(core: CoreStart, plugins: StartPlugins) {
const { links, getFilteredLinks } = await this.lazyApplicationLinks();
const { license$ } = plugins.licensing;
const { upsellingService, appLinksSwitcher, deepLinksFormatter } = this.contract;
registerDeepLinksUpdater(this.appUpdater$, deepLinksFormatter);
const baseLinksPermissions: LinksPermissions = {
experimentalFeatures: this.experimentalFeatures,
upselling: upsellingService,
capabilities: core.application.capabilities,
};
license$
.pipe(
mergeMap(async (license) => {
const linksPermissions: LinksPermissions = {
...baseLinksPermissions,
...(license.type != null && { license }),
};
// set initial links to not block rendering
updateAppLinks(appLinksSwitcher(links), linksPermissions);
// set filtered links asynchronously
const filteredLinks = await getFilteredLinks(core, plugins);
updateAppLinks(appLinksSwitcher(filteredLinks), linksPermissions);
})
)
.subscribe();
}
private async getIsSidebarEnabled(core: CoreSetup) {
const [coreStart] = await core.getStartServices();
const chromeStyle = await firstValueFrom(coreStart.chrome.getChromeStyle$());
return chromeStyle === 'classic';
}
}

View file

@ -6,13 +6,11 @@
*/
import { BehaviorSubject } from 'rxjs';
import type { RouteProps } from 'react-router-dom';
import { UpsellingService } from '@kbn/security-solution-upselling/service';
import type { CoreStart } from '@kbn/core/public';
import type { ContractStartServices, PluginSetup, PluginStart } from './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 { navLinks$, updateNavLinks } from './common/links/nav_links';
import { breadcrumbsNav$ } from './common/breadcrumbs';
import { ContractComponentsService } from './contract_components';
import { OnboardingPageService } from './app/components/onboarding/onboarding_page_service';
@ -21,66 +19,64 @@ export class PluginContract {
public componentsService: ContractComponentsService;
public upsellingService: UpsellingService;
public onboardingPageService: OnboardingPageService;
public extraRoutes$: BehaviorSubject<RouteProps[]>;
public appLinksSwitcher: AppLinksSwitcher;
public deepLinksFormatter?: DeepLinksFormatter;
public isSolutionNavigationEnabled$: BehaviorSubject<boolean>;
constructor(private readonly experimentalFeatures: ExperimentalFeatures) {
this.extraRoutes$ = new BehaviorSubject<RouteProps[]>([]);
this.onboardingPageService = new OnboardingPageService();
this.componentsService = new ContractComponentsService();
this.upsellingService = new UpsellingService();
this.appLinksSwitcher = (appLinks) => appLinks;
}
public getStartServices(): ContractStartServices {
return {
extraRoutes$: this.extraRoutes$.asObservable(),
getComponents$: this.componentsService.getComponents$.bind(this.componentsService),
upselling: this.upsellingService,
onboarding: this.onboardingPageService,
};
this.isSolutionNavigationEnabled$ = new BehaviorSubject<boolean>(false); // defaults to classic navigation
}
public getSetupContract(): PluginSetup {
return {
resolver: lazyResolver,
experimentalFeatures: { ...this.experimentalFeatures },
setAppLinksSwitcher: (appLinksSwitcher) => {
this.appLinksSwitcher = appLinksSwitcher;
},
setDeepLinksFormatter: (deepLinksFormatter) => {
this.deepLinksFormatter = deepLinksFormatter;
},
};
}
public getStartContract(): PluginStart {
public getStartContract(core: CoreStart): PluginStart {
return {
setOnboardingPageSettings: this.onboardingPageService,
getNavLinks$: () => navLinks$,
setExtraRoutes: (extraRoutes) => this.extraRoutes$.next(extraRoutes),
setComponents: (components) => {
this.componentsService.setComponents(components);
},
getBreadcrumbsNav$: () => breadcrumbsNav$,
getUpselling: () => this.upsellingService,
// TODO: remove the following APIs after rollout https://github.com/elastic/kibana/issues/179572
setIsSolutionNavigationEnabled: (isSolutionNavigationEnabled) => {
this.isSolutionNavigationEnabled$.next(isSolutionNavigationEnabled);
updateNavLinks(isSolutionNavigationEnabled, core);
},
getSolutionNavigation: () => lazySolutionNavigation(core),
};
}
public getStopContract() {
return {};
public getStartServices(): ContractStartServices {
return {
getComponents$: this.componentsService.getComponents$.bind(this.componentsService),
upselling: this.upsellingService,
onboarding: this.onboardingPageService,
};
}
}
/**
* 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 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();
};
const lazySolutionNavigation = async (core: CoreStart) => {
const { getSolutionNavigation } = await import(
/* webpackChunkName: "solution_navigation" */
'./app/solution_navigation'
);
return getSolutionNavigation(core);
};

View file

@ -0,0 +1,153 @@
/*
* 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 { AppMountParameters, CoreSetup, CoreStart, PackageInfo } from '@kbn/core/public';
import { FilterManager, NowProvider, QueryService } from '@kbn/data-plugin/public';
import type { DataPublicPluginStart, QueryStart } from '@kbn/data-plugin/public';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { initTelemetry, TelemetryService } from './common/lib/telemetry';
import { KibanaServices } from './common/lib/kibana/services';
import type { ExperimentalFeatures } from '../common/experimental_features';
import { licenseService } from './common/hooks/use_license';
import { ExperimentalFeaturesService } from './common/experimental_features_service';
import type { PluginContract } from './plugin_contract';
import type { ConfigSettings } from '../common/config_settings';
import { parseConfigSettings } from '../common/config_settings';
import { APP_UI_ID } from '../common/constants';
import { TopValuesPopoverService } from './app/components/top_values_popover/top_values_popover_service';
import type { SecuritySolutionUiConfigType } from './common/types';
import type {
PluginStart,
SetupPlugins,
StartPlugins,
StartPluginsDependencies,
StartServices,
} from './types';
export class PluginServices {
private readonly telemetry: TelemetryService = new TelemetryService();
private readonly queryService: QueryService = new QueryService();
private readonly storage = new Storage(localStorage);
private readonly sessionStorage = new Storage(sessionStorage);
private readonly configSettings: ConfigSettings;
/**
* For internal use. Specify which version of the Detection Rules fleet package to install
* when upgrading rules. If not provided, the latest compatible package will be installed,
* or if running from a dev environment or -SNAPSHOT build, the latest pre-release package
* will be used (if fleet is available or not within an airgapped environment).
*
* Note: This is for `upgrade only`, which occurs by means of the `useUpgradeSecurityPackages`
* hook when navigating to a Security Solution page. The package version specified in
* `fleet_packages.json` in project root will always be installed first on Kibana start if
* the package is not already installed.
*/
private readonly prebuiltRulesPackageVersion: string | undefined;
constructor(
private readonly config: SecuritySolutionUiConfigType,
private readonly experimentalFeatures: ExperimentalFeatures,
private readonly contract: PluginContract,
private readonly packageInfo: PackageInfo
) {
this.configSettings = parseConfigSettings(this.config.offeringSettings ?? {}).settings;
this.prebuiltRulesPackageVersion = this.config.prebuiltRulesPackageVersion;
}
public setup(
coreSetup: CoreSetup<StartPluginsDependencies, PluginStart>,
pluginsSetup: SetupPlugins
) {
initTelemetry({ usageCollection: pluginsSetup.usageCollection }, APP_UI_ID);
this.telemetry.setup(
{ analytics: coreSetup.analytics },
{ prebuiltRulesPackageVersion: this.prebuiltRulesPackageVersion }
);
this.queryService?.setup({
uiSettings: coreSetup.uiSettings,
storage: this.storage,
nowProvider: new NowProvider(),
});
}
public start(coreStart: CoreStart, pluginsStart: StartPlugins) {
ExperimentalFeaturesService.init({ experimentalFeatures: this.experimentalFeatures });
licenseService.start(pluginsStart.licensing.license$);
KibanaServices.init({
...coreStart,
...pluginsStart,
kibanaBranch: this.packageInfo.branch,
kibanaVersion: this.packageInfo.version,
buildFlavor: this.packageInfo.buildFlavor,
prebuiltRulesPackageVersion: this.prebuiltRulesPackageVersion,
});
}
public stop() {
this.queryService.stop();
licenseService.stop();
}
public async generateServices(
coreStart: CoreStart,
startPlugins: StartPluginsDependencies,
params: AppMountParameters<unknown>
): Promise<StartServices> {
const { apm } = await import('@elastic/apm-rum');
const { SecuritySolutionTemplateWrapper } = await import('./app/home/template_wrapper');
const { savedObjectsTaggingOss, ...plugins } = startPlugins;
const query = this.queryService.start({
uiSettings: coreStart.uiSettings,
storage: this.storage,
http: coreStart.http,
});
const customDataService = this.startCustomDataService(query, startPlugins.data);
return {
...coreStart,
...plugins,
...this.contract.getStartServices(),
apm,
configSettings: this.configSettings,
savedObjectsTagging: savedObjectsTaggingOss.getTaggingApi(),
setHeaderActionMenu: params.setHeaderActionMenu,
storage: this.storage,
sessionStorage: this.sessionStorage,
security: startPlugins.security,
onAppLeave: params.onAppLeave,
securityLayout: { getPluginWrapper: () => SecuritySolutionTemplateWrapper },
contentManagement: startPlugins.contentManagement,
telemetry: this.telemetry.start(),
customDataService,
topValuesPopover: new TopValuesPopoverService(),
timelineFilterManager: new FilterManager(coreStart.uiSettings),
};
}
public getExperimentalFeatures() {
return this.experimentalFeatures;
}
private startCustomDataService = (query: QueryStart, data: DataPublicPluginStart) => {
// used for creating a custom stateful KQL Query Bar
const customDataService: DataPublicPluginStart = {
...data,
query,
// @ts-expect-error
_name: 'custom',
};
// @ts-expect-error
customDataService.query.filterManager._name = 'customFilterManager';
return customDataService;
};
}

View file

@ -51,7 +51,6 @@ import type { DataViewsServicePublic } from '@kbn/data-views-plugin/public';
import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public';
import type { ExpressionsStart } from '@kbn/expressions-plugin/public';
import type { RouteProps } from 'react-router-dom';
import type { DiscoverStart } from '@kbn/discover-plugin/public';
import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public';
import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
@ -73,18 +72,21 @@ import type { CloudDefend } from './cloud_defend';
import type { ThreatIntelligence } from './threat_intelligence';
import type { SecuritySolutionTemplateWrapper } from './app/home/template_wrapper';
import type { Explore } from './explore';
import type { AppLinksSwitcher, NavigationLink } from './common/links';
import type { NavigationLink } from './common/links';
import type { EntityAnalytics } from './entity_analytics';
import type { Assets } from './assets';
import type { Investigations } from './investigations';
import type { MachineLearning } from './machine_learning';
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 { SetComponents, GetComponents$ } from './contract_components';
import type { ConfigSettings } from '../common/config_settings';
import type { OnboardingPageService } from './app/components/onboarding/onboarding_page_service';
import type { SolutionNavigation } from './app/solution_navigation/solution_navigation';
export interface SetupPlugins {
cloud?: CloudSetup;
@ -151,7 +153,6 @@ export interface StartPluginsDependencies extends StartPlugins {
}
export interface ContractStartServices {
extraRoutes$: Observable<RouteProps[]>;
getComponents$: GetComponents$;
upselling: UpsellingService;
onboarding: OnboardingPageService;
@ -185,17 +186,16 @@ export type StartServices = CoreStart &
export interface PluginSetup {
resolver: () => Promise<ResolverPluginSetup>;
experimentalFeatures: ExperimentalFeatures;
setAppLinksSwitcher: (appLinksSwitcher: AppLinksSwitcher) => void;
setDeepLinksFormatter: (deepLinksFormatter: DeepLinksFormatter) => void;
}
export interface PluginStart {
getNavLinks$: () => Observable<NavigationLink[]>;
setExtraRoutes: (extraRoutes: RouteProps[]) => void;
setComponents: SetComponents;
getBreadcrumbsNav$: () => Observable<BreadcrumbsNav>;
getUpselling: () => UpsellingService;
setOnboardingPageSettings: OnboardingPageService;
setIsSolutionNavigationEnabled: (isSolutionNavigationEnabled: boolean) => void;
getSolutionNavigation: () => Promise<SolutionNavigation>;
}
export type InspectResponse = Inspect & { response: string[] };
@ -217,6 +217,9 @@ export interface SubPlugins {
threatIntelligence: ThreatIntelligence;
timelines: Timelines;
entityAnalytics: EntityAnalytics;
assets: Assets;
investigations: Investigations;
machineLearning: MachineLearning;
}
// TODO: find a better way to defined these types
@ -235,4 +238,7 @@ export interface StartedSubPlugins {
threatIntelligence: ReturnType<ThreatIntelligence['start']>;
timelines: ReturnType<Timelines['start']>;
entityAnalytics: ReturnType<EntityAnalytics['start']>;
assets: ReturnType<Assets['start']>;
investigations: ReturnType<Investigations['start']>;
machineLearning: ReturnType<MachineLearning['start']>;
}

View file

@ -194,6 +194,8 @@
"@kbn/core-ui-settings-server",
"@kbn/core-http-request-handler-context-server",
"@kbn/core-http-server-mocks",
"@kbn/data-service"
"@kbn/data-service",
"@kbn/core-chrome-browser",
"@kbn/shared-ux-chrome-navigation"
]
}

View file

@ -10,6 +10,8 @@
"configPath": ["xpack", "securitySolutionEss"],
"requiredPlugins": [
"securitySolution",
"management",
"navigation",
"licensing",
],
"optionalPlugins": [

View file

@ -8,10 +8,14 @@
import { coreMock } from '@kbn/core/public/mocks';
import { securitySolutionMock } from '@kbn/security-solution-plugin/public/mocks';
import { licensingMock } from '@kbn/licensing-plugin/public/mocks';
import { navigationPluginMock } from '@kbn/navigation-plugin/public/mocks';
import { managementPluginMock } from '@kbn/management-plugin/public/mocks';
import type { Services } from '../services';
export const mockServices: Services = {
...coreMock.createStart(),
securitySolution: securitySolutionMock.createStart(),
licensing: licensingMock.createStart(),
navigation: navigationPluginMock.createStartContract(),
management: managementPluginMock.createStartContract(),
};

View file

@ -0,0 +1,17 @@
/*
* 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 { Services } from '../common/services';
import { subscribeBreadcrumbs } from './breadcrumbs';
import { enableManagementCardsLanding } from './management_cards';
import { initSideNavigation } from './side_navigation';
export const startNavigation = (services: Services) => {
initSideNavigation(services);
subscribeBreadcrumbs(services);
enableManagementCardsLanding(services);
};

View file

@ -0,0 +1,51 @@
/*
* 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 { CardNavExtensionDefinition } from '@kbn/management-cards-navigation';
import {
getNavigationPropsFromId,
SecurityPageName,
ExternalPageName,
} from '@kbn/security-solution-navigation';
import { combineLatestWith } from 'rxjs';
import type { Services } from '../common/services';
const SecurityManagementCards = new Map<string, CardNavExtensionDefinition['category']>([
[ExternalPageName.visualize, 'content'],
[ExternalPageName.maps, 'content'],
[SecurityPageName.entityAnalyticsManagement, 'alerts'],
]);
export const enableManagementCardsLanding = (services: Services) => {
const { securitySolution, management, application, navigation } = services;
securitySolution
.getNavLinks$()
.pipe(combineLatestWith(navigation.isSolutionNavEnabled$))
.subscribe(([navLinks, isSolutionNavEnabled]) => {
const cardNavDefinitions = navLinks.reduce<Record<string, CardNavExtensionDefinition>>(
(acc, navLink) => {
if (SecurityManagementCards.has(navLink.id)) {
const { appId, deepLinkId, path } = getNavigationPropsFromId(navLink.id);
acc[navLink.id] = {
category: SecurityManagementCards.get(navLink.id) ?? 'other',
title: navLink.title,
description: navLink.description ?? '',
icon: navLink.landingIcon ?? '',
href: application.getUrlForApp(appId, { deepLinkId, path }),
skipValidation: true,
};
}
return acc;
},
{}
);
management.setupCardsNavigation({
enabled: isSolutionNavEnabled,
extendCardNavDefinitions: cardNavDefinitions,
});
});
};

View file

@ -0,0 +1,112 @@
/*
* 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 } from 'rxjs';
import produce from 'immer';
import { i18n } from '@kbn/i18n';
import { SecurityPageName, SECURITY_UI_APP_ID } from '@kbn/security-solution-navigation';
import type { AppDeepLinkId, GroupDefinition, NodeDefinition } from '@kbn/core-chrome-browser';
import { type Services } from '../common/services';
export const SOLUTION_NAME = i18n.translate('xpack.securitySolutionEss.nav.solutionName', {
defaultMessage: 'Security',
});
export const initSideNavigation = async (services: Services) => {
const { securitySolution, navigation } = services;
navigation.isSolutionNavEnabled$.subscribe((isSolutionNavigationEnabled) => {
securitySolution.setIsSolutionNavigationEnabled(isSolutionNavigationEnabled);
});
const { navigationTree$, panelContentProvider } = await securitySolution.getSolutionNavigation();
const essNavigationTree$ = navigationTree$.pipe(
map((navigationTree) =>
produce(navigationTree, (draft) => {
const footerGroup: GroupDefinition | undefined = draft.footer?.find(
({ type }) => type === 'navGroup'
) as GroupDefinition;
const management = footerGroup?.children.find((child) => child.link === 'management');
if (management) {
management.renderAs = 'panelOpener';
management.id = 'stack_management';
management.spaceBefore = null;
management.children = stackManagementLinks;
}
})
)
);
navigation.addSolutionNavigation({
id: 'security',
homePage: `${SECURITY_UI_APP_ID}:${SecurityPageName.landing}`,
title: SOLUTION_NAME,
icon: 'logoSecurity',
navigationTree$: essNavigationTree$,
panelContentProvider,
dataTestSubj: 'securitySolutionSideNav',
});
};
// Temporary configuration to render the stack management links in the panel
const stackManagementLinks: Array<NodeDefinition<AppDeepLinkId, string, string>> = [
{
title: 'Ingest',
children: [{ link: 'management:ingest_pipelines' }, { link: 'management:pipelines' }],
},
{
title: 'Data',
children: [
{ link: 'management:index_management' },
{ link: 'management:index_lifecycle_management' },
{ link: 'management:snapshot_restore' },
{ link: 'management:rollup_jobs' },
{ link: 'management:transform' },
{ link: 'management:cross_cluster_replication' },
{ link: 'management:remote_clusters' },
{ link: 'management:migrate_data' },
],
},
{
title: 'Alerts and Insights',
children: [
{ link: 'management:triggersActions' },
{ link: 'management:cases' },
{ link: 'management:triggersActionsConnectors' },
{ link: 'management:reporting' },
{ link: 'management:jobsListLink' },
{ link: 'management:watcher' },
{ link: 'management:maintenanceWindows' },
],
},
{
title: 'Security',
children: [
{ link: 'management:users' },
{ link: 'management:roles' },
{ link: 'management:api_keys' },
{ link: 'management:role_mappings' },
],
},
{
title: 'Kibana',
children: [
{ link: 'management:dataViews' },
{ link: 'management:filesManagement' },
{ link: 'management:objects' },
{ link: 'management:tags' },
{ link: 'management:search_sessions' },
{ link: 'management:spaces' },
{ link: 'management:settings' },
],
},
{
title: 'Stack',
children: [{ link: 'management:license_management' }, { link: 'management:upgrade_assistant' }],
},
];

View file

@ -6,7 +6,7 @@
*/
import type { CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
import { subscribeBreadcrumbs } from './breadcrumbs';
import { startNavigation } from './navigation';
import { createServices } from './common/services';
import { registerUpsellings } from './upselling/register_upsellings';
import type {
@ -40,14 +40,13 @@ export class SecuritySolutionEssPlugin
const { securitySolution, licensing } = startDeps;
const services = createServices(core, startDeps);
startNavigation(services);
setOnboardingSettings(services);
licensing.license$.subscribe((license) => {
registerUpsellings(securitySolution.getUpselling(), license, services);
});
setOnboardingSettings(services);
subscribeBreadcrumbs(services);
return {};
}

View file

@ -11,6 +11,8 @@ import type {
} from '@kbn/security-solution-plugin/public';
import type { CloudExperimentsPluginStart } from '@kbn/cloud-experiments-plugin/common';
import type { LicensingPluginStart } from '@kbn/licensing-plugin/public';
import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public';
import type { ManagementStart } from '@kbn/management-plugin/public';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface SecuritySolutionEssPluginSetup {}
@ -26,4 +28,6 @@ export interface SecuritySolutionEssPluginStartDeps {
securitySolution: SecuritySolutionPluginStart;
cloudExperiments?: CloudExperimentsPluginStart;
licensing: LicensingPluginStart;
navigation: NavigationPublicPluginStart;
management: ManagementStart;
}

View file

@ -23,5 +23,10 @@
"@kbn/security-solution-navigation",
"@kbn/licensing-plugin",
"@kbn/security-solution-upselling",
"@kbn/i18n",
"@kbn/navigation-plugin",
"@kbn/management-cards-navigation",
"@kbn/management-plugin",
"@kbn/core-chrome-browser",
]
}

View file

@ -26,8 +26,5 @@
"optionalPlugins": [
"securitySolutionEss"
],
"requiredBundles": [
"usageCollection"
]
}
}

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