,
+ pageName: SecurityPageName,
+ redirectOnMissing?: boolean
+) => {
+ return function WithSecurityRoutePageWrapper(props: T) {
+ return (
+
+
+
+ );
+ };
+};
diff --git a/x-pack/plugins/security_solution/public/common/components/with_lazy_hook/index.tsx b/x-pack/plugins/security_solution/public/common/components/with_lazy_hook/index.tsx
new file mode 100644
index 0000000000000..265eb7f1e5f65
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/with_lazy_hook/index.tsx
@@ -0,0 +1,25 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import React, { useEffect, useState } from 'react';
+
+export const withLazyHook = (
+ Component: React.ComponentType
,
+ hookImport: () => Promise>,
+ fallback: React.ReactNode = null
+) => {
+ return React.memo>(function WithLazyHook(props) {
+ const [lazyHookProp, setLazyHookProp] = useState>();
+
+ useEffect(() => {
+ hookImport().then((hook) => {
+ setLazyHookProp(() => hook);
+ });
+ }, []);
+
+ return lazyHookProp ? : <>{fallback}>;
+ });
+};
diff --git a/x-pack/plugins/security_solution/public/common/constants.ts b/x-pack/plugins/security_solution/public/common/constants.ts
index 8590646082a9c..c114f70915a75 100644
--- a/x-pack/plugins/security_solution/public/common/constants.ts
+++ b/x-pack/plugins/security_solution/public/common/constants.ts
@@ -17,4 +17,4 @@ export const RISK_SCORE_MEDIUM = 47;
export const RISK_SCORE_HIGH = 73;
export const RISK_SCORE_CRITICAL = 99;
-export const INGESTION_HUB_VIDEO_SOURCE = '//play.vidyard.com/K6kKDBbP9SpXife9s2tHNP.html?';
+export const ONBOARDING_VIDEO_SOURCE = '//play.vidyard.com/K6kKDBbP9SpXife9s2tHNP.html?';
diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/onboarding/types.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/onboarding/types.ts
index f6f52a6d675d2..224635715b324 100644
--- a/x-pack/plugins/security_solution/public/common/lib/telemetry/events/onboarding/types.ts
+++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/events/onboarding/types.ts
@@ -5,7 +5,6 @@
* 2.0.
*/
import type { RootSchema } from '@kbn/core/public';
-import type { StepLinkId } from '../../../../components/landing_page/onboarding/step_links/types';
import type { TelemetryEventTypes } from '../../constants';
export type OnboardingHubStepOpenTrigger = 'navigation' | 'click';
@@ -22,7 +21,7 @@ export interface OnboardingHubStepOpen {
export interface OnboardingHubStepLinkClickedParams {
originStepId: string;
- stepLinkId: StepLinkId;
+ stepLinkId: string;
}
export interface OnboardingHubStepLinkClicked {
@@ -34,7 +33,7 @@ export type OnboardingHubStepFinishedTrigger = 'auto_check' | 'click';
export interface OnboardingHubStepFinishedParams {
stepId: string;
- stepLinkId?: StepLinkId;
+ stepLinkId?: string;
trigger: OnboardingHubStepFinishedTrigger;
}
diff --git a/x-pack/plugins/security_solution/public/index.ts b/x-pack/plugins/security_solution/public/index.ts
index 61c56ad5a36c5..92fcf14ace691 100644
--- a/x-pack/plugins/security_solution/public/index.ts
+++ b/x-pack/plugins/security_solution/public/index.ts
@@ -16,12 +16,3 @@ export const plugin = (context: PluginInitializerContext): Plugin => new Plugin(
export type { PluginSetup, PluginStart };
export { Plugin };
-
-export {
- CreateProjectSteps,
- OverviewSteps,
- AddIntegrationsSteps,
- ViewDashboardSteps,
- EnablePrebuiltRulesSteps,
- ViewAlertsSteps,
-} from './common/components/landing_page/onboarding/types';
diff --git a/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx b/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx
index 166d14b01c1a1..8bbba3885a2ab 100644
--- a/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx
+++ b/x-pack/plugins/security_solution/public/lazy_sub_plugins.tsx
@@ -16,6 +16,7 @@ import { Detections } from './detections';
import { Exceptions } from './exceptions';
import { Explore } from './explore';
import { Kubernetes } from './kubernetes';
+import { Onboarding } from './onboarding';
import { Overview } from './overview';
import { Rules } from './rules';
import { Timelines } from './timelines';
@@ -39,6 +40,7 @@ const subPluginClasses = {
Exceptions,
Explore,
Kubernetes,
+ Onboarding,
Overview,
Rules,
Timelines,
diff --git a/x-pack/plugins/security_solution/public/mocks.ts b/x-pack/plugins/security_solution/public/mocks.ts
index f279e82804064..8e16b883f474b 100644
--- a/x-pack/plugins/security_solution/public/mocks.ts
+++ b/x-pack/plugins/security_solution/public/mocks.ts
@@ -11,15 +11,15 @@ import type { BreadcrumbsNav } from './common/breadcrumbs';
import type { NavigationLink } from './common/links/types';
import { allowedExperimentalValues } from '../common/experimental_features';
import type { PluginStart, PluginSetup, ContractStartServices } from './types';
-import { OnboardingPageService } from './app/components/onboarding/onboarding_page_service';
+import { OnboardingService } from './onboarding/service';
const upselling = new UpsellingService();
-const onboardingPageService = new OnboardingPageService();
+const onboardingService = new OnboardingService();
export const contractStartServicesMock: ContractStartServices = {
getComponents$: jest.fn(() => of({})),
upselling,
- onboarding: onboardingPageService,
+ onboarding: onboardingService,
};
const setupMock = (): PluginSetup => ({
@@ -34,7 +34,7 @@ const startMock = (): PluginStart => ({
() => new BehaviorSubject({ leading: [], trailing: [] })
),
getUpselling: () => upselling,
- setOnboardingPageSettings: onboardingPageService,
+ setOnboardingSettings: onboardingService.setSettings.bind(onboardingService),
setIsSolutionNavigationEnabled: jest.fn(),
getSolutionNavigation: jest.fn(async () => ({
navigationTree$: of({ body: [], footer: [] }),
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/__mocks__/onboarding_context.tsx b/x-pack/plugins/security_solution/public/onboarding/components/__mocks__/onboarding_context.tsx
new file mode 100644
index 0000000000000..d1c9afcef33d6
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/__mocks__/onboarding_context.tsx
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+/*
+ * 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 { OnboardingContextValue } from '../onboarding_context';
+import {
+ mockReportCardOpen,
+ mockReportCardComplete,
+ mockReportCardLinkClicked,
+} from './onboarding_context_mocks';
+
+export const useOnboardingContext = (): OnboardingContextValue => {
+ return {
+ spaceId: 'default',
+ reportCardOpen: mockReportCardOpen,
+ reportCardComplete: mockReportCardComplete,
+ reportCardLinkClicked: mockReportCardLinkClicked,
+ };
+};
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/__mocks__/onboarding_context_mocks.ts b/x-pack/plugins/security_solution/public/onboarding/components/__mocks__/onboarding_context_mocks.ts
new file mode 100644
index 0000000000000..dcd5d681b34bf
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/__mocks__/onboarding_context_mocks.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export const mockReportCardOpen = jest.fn();
+export const mockReportCardComplete = jest.fn();
+export const mockReportCardLinkClicked = jest.fn();
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding.tsx
new file mode 100644
index 0000000000000..17f4840e68dc4
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding.tsx
@@ -0,0 +1,56 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+
+import { EuiSpacer, useEuiTheme } from '@elastic/eui';
+import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
+import { PluginTemplateWrapper } from '../../common/components/plugin_template_wrapper';
+import { CenteredLoadingSpinner } from '../../common/components/centered_loading_spinner';
+import { useSpaceId } from '../../common/hooks/use_space_id';
+import { OnboardingContextProvider } from './onboarding_context';
+import { OnboardingAVCBanner } from './onboarding_banner';
+import { OnboardingHeader } from './onboarding_header';
+import { OnboardingBody } from './onboarding_body';
+import { OnboardingFooter } from './onboarding_footer';
+import { PAGE_CONTENT_WIDTH } from '../constants';
+
+export const OnboardingPage = React.memo(() => {
+ const spaceId = useSpaceId();
+ const { euiTheme } = useEuiTheme();
+
+ if (!spaceId) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+});
+OnboardingPage.displayName = 'OnboardingPage';
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_banner/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_banner/index.ts
new file mode 100644
index 0000000000000..cfb8418f5e038
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_banner/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { OnboardingBanner as OnboardingAVCBanner } from './onboarding_banner';
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_banner/onboarding_banner.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_banner/onboarding_banner.tsx
new file mode 100644
index 0000000000000..201fae862b43c
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_banner/onboarding_banner.tsx
@@ -0,0 +1,27 @@
+/*
+ * 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, { useCallback } from 'react';
+
+import { AVCResultsBanner2024, useIsStillYear2024 } from '@kbn/avc-banner';
+import { useStoredIsAVCBannerDismissed } from '../../hooks/use_stored_state';
+
+export const OnboardingBanner = React.memo(() => {
+ const [isAVCBannerDismissed, setIsAVCBannerDismissed] = useStoredIsAVCBannerDismissed();
+ const isStillYear2024 = useIsStillYear2024();
+
+ const dismissAVCBanner = useCallback(() => {
+ setIsAVCBannerDismissed(true);
+ }, [setIsAVCBannerDismissed]);
+
+ if (isAVCBannerDismissed || !isStillYear2024) {
+ return null;
+ }
+
+ return ;
+});
+OnboardingBanner.displayName = 'OnboardingBanner';
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/body_config.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/body_config.ts
new file mode 100644
index 0000000000000..7f97b5c8eacd1
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/body_config.ts
@@ -0,0 +1,36 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { i18n } from '@kbn/i18n';
+import type { OnboardingGroupConfig } from '../../types';
+import { integrationsCardConfig } from './cards/integrations';
+import { dashboardsCardConfig } from './cards/dashboards';
+import { rulesCardConfig } from './cards/rules';
+import { alertsCardConfig } from './cards/alerts';
+import { assistantCardConfig } from './cards/assistant';
+
+export const bodyConfig: OnboardingGroupConfig[] = [
+ {
+ title: i18n.translate('xpack.securitySolution.onboarding.dataGroup.title', {
+ defaultMessage: 'Ingest your data',
+ }),
+ cards: [integrationsCardConfig, dashboardsCardConfig],
+ },
+ {
+ title: i18n.translate('xpack.securitySolution.onboarding.alertsGroup.title', {
+ defaultMessage: 'Configure rules and alerts',
+ }),
+ cards: [rulesCardConfig, alertsCardConfig],
+ },
+ {
+ title: i18n.translate('xpack.securitySolution.onboarding.discoverGroup.title', {
+ defaultMessage: 'Discover Elastic AI',
+ }),
+ // TODO: Add attackDiscoveryCardConfig when it is ready (https://github.com/elastic/kibana/issues/189487)
+ cards: [assistantCardConfig],
+ },
+];
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/alerts_card.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/alerts_card.test.tsx
new file mode 100644
index 0000000000000..3e83bcb851f82
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/alerts_card.test.tsx
@@ -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 React from 'react';
+import { render } from '@testing-library/react';
+import { AlertsCard } from './alerts_card';
+import { TestProviders } from '../../../../../common/mock';
+
+const props = {
+ setComplete: jest.fn(),
+ checkComplete: jest.fn(),
+ isCardComplete: jest.fn(),
+ setExpandedCardId: jest.fn(),
+};
+
+describe('AlertsCard', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('description should be in the document', () => {
+ const { getByTestId } = render(
+
+
+
+ );
+
+ expect(getByTestId('alertsCardDescription')).toBeInTheDocument();
+ });
+
+ it('card callout should be rendered if integrations cards is not complete', () => {
+ props.isCardComplete.mockReturnValueOnce(false);
+
+ const { getByText } = render(
+
+
+
+ );
+
+ expect(getByText('To view alerts add integrations first.')).toBeInTheDocument();
+ });
+
+ it('card button should be disabled if integrations cards is not complete', () => {
+ props.isCardComplete.mockReturnValueOnce(false);
+
+ const { getByTestId } = render(
+
+
+
+ );
+
+ expect(getByTestId('alertsCardButton').querySelector('button')).toBeDisabled();
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/alerts_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/alerts_card.tsx
new file mode 100644
index 0000000000000..c0369ed23d61c
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/alerts_card.tsx
@@ -0,0 +1,82 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useCallback, useMemo } from 'react';
+import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
+import { SecurityPageName } from '@kbn/security-solution-navigation';
+import { SecuritySolutionLinkButton } from '../../../../../common/components/links';
+import { OnboardingCardId } from '../../../../constants';
+import type { OnboardingCardComponent } from '../../../../types';
+import { OnboardingCardContentImagePanel } from '../common/card_content_image_panel';
+import { CardCallOut } from '../common/card_callout';
+import alertsImageSrc from './images/alerts.png';
+import * as i18n from './translations';
+
+export const AlertsCard: OnboardingCardComponent = ({
+ isCardComplete,
+ setExpandedCardId,
+ setComplete,
+}) => {
+ const isIntegrationsCardComplete = useMemo(
+ () => isCardComplete(OnboardingCardId.integrations),
+ [isCardComplete]
+ );
+
+ const expandIntegrationsCard = useCallback(() => {
+ setExpandedCardId(OnboardingCardId.integrations, { scroll: true });
+ }, [setExpandedCardId]);
+
+ return (
+
+
+
+
+ {i18n.ALERTS_CARD_DESCRIPTION}
+
+ {!isIntegrationsCardComplete && (
+ <>
+
+
+
+ {i18n.ALERTS_CARD_CALLOUT_INTEGRATIONS_BUTTON}
+
+
+
+
+
+ }
+ />
+ >
+ )}
+
+
+ setComplete(true)}
+ deepLinkId={SecurityPageName.alerts}
+ fill
+ isDisabled={!isIntegrationsCardComplete}
+ >
+ {i18n.ALERTS_CARD_VIEW_ALERTS_BUTTON}
+
+
+
+
+ );
+};
+
+// eslint-disable-next-line import/no-default-export
+export default AlertsCard;
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/images/alerts.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/images/alerts.png
new file mode 100644
index 0000000000000..6eaf13bfc7b53
Binary files /dev/null and b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/images/alerts.png differ
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/images/alerts_icon.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/images/alerts_icon.png
new file mode 100644
index 0000000000000..e1013a6eae7fc
Binary files /dev/null and b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/images/alerts_icon.png differ
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/index.ts
new file mode 100644
index 0000000000000..dda6e97b82606
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/index.ts
@@ -0,0 +1,25 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import type { OnboardingCardConfig } from '../../../../types';
+import { OnboardingCardId } from '../../../../constants';
+import { ALERTS_CARD_TITLE } from './translations';
+import alertsIcon from './images/alerts_icon.png';
+
+export const alertsCardConfig: OnboardingCardConfig = {
+ id: OnboardingCardId.alerts,
+ title: ALERTS_CARD_TITLE,
+ icon: alertsIcon,
+ Component: React.lazy(
+ () =>
+ import(
+ /* webpackChunkName: "onboarding_alerts_card" */
+ './alerts_card'
+ )
+ ),
+};
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/translations.ts
new file mode 100644
index 0000000000000..3138f01d20b66
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/alerts/translations.ts
@@ -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 { i18n } from '@kbn/i18n';
+
+export const ALERTS_CARD_TITLE = i18n.translate(
+ 'xpack.securitySolution.onboarding.alertsCard.title',
+ {
+ defaultMessage: 'View alerts',
+ }
+);
+
+export const ALERTS_CARD_DESCRIPTION = i18n.translate(
+ 'xpack.securitySolution.onboarding.alertsCard.description',
+ {
+ defaultMessage:
+ 'Visualize, sort, filter, and investigate alerts from across your infrastructure. Examine individual alerts of interest, and discover general patterns in alert volume and severity.',
+ }
+);
+
+export const ALERTS_CARD_CALLOUT_INTEGRATIONS_TEXT = i18n.translate(
+ 'xpack.securitySolution.onboarding.alertsCard.calloutIntegrationsText',
+ {
+ defaultMessage: 'To view alerts add integrations first.',
+ }
+);
+
+export const ALERTS_CARD_CALLOUT_INTEGRATIONS_BUTTON = i18n.translate(
+ 'xpack.securitySolution.onboarding.alertsCard.calloutIntegrationsButton',
+ {
+ defaultMessage: 'Add integrations step',
+ }
+);
+
+export const ALERTS_CARD_VIEW_ALERTS_BUTTON = i18n.translate(
+ 'xpack.securitySolution.onboarding.alertsCard.viewAlertsButton',
+ {
+ defaultMessage: 'View alerts',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_card.tsx
new file mode 100644
index 0000000000000..4b87f23dd2435
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_card.tsx
@@ -0,0 +1,77 @@
+/*
+ * 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, { useCallback, useMemo } from 'react';
+import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiText } from '@elastic/eui';
+import { css } from '@emotion/css';
+import { OnboardingCardId } from '../../../../constants';
+import type { OnboardingCardComponent } from '../../../../types';
+import * as i18n from './translations';
+import { OnboardingCardContentPanel } from '../common/card_content_panel';
+import { ConnectorCards } from './connectors/connector_cards';
+import { CardCallOut } from '../common/card_callout';
+import type { AssistantCardMetadata } from './types';
+
+export const AssistantCard: OnboardingCardComponent = ({
+ isCardComplete,
+ setExpandedCardId,
+ checkCompleteMetadata,
+ checkComplete,
+}) => {
+ const isIntegrationsCardComplete = useMemo(
+ () => isCardComplete(OnboardingCardId.integrations),
+ [isCardComplete]
+ );
+
+ const expandIntegrationsCard = useCallback(() => {
+ setExpandedCardId(OnboardingCardId.integrations, { scroll: true });
+ }, [setExpandedCardId]);
+
+ const connectors = checkCompleteMetadata?.connectors;
+
+ return (
+
+
+
+
+ {i18n.ASSISTANT_CARD_DESCRIPTION}
+
+
+
+ {isIntegrationsCardComplete ? (
+
+ ) : (
+
+
+
+ {i18n.ASSISTANT_CARD_CALLOUT_INTEGRATIONS_BUTTON}
+
+
+
+
+
+ }
+ />
+
+ )}
+
+
+
+ );
+};
+
+// eslint-disable-next-line import/no-default-export
+export default AssistantCard;
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_check_complete.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_check_complete.ts
new file mode 100644
index 0000000000000..bdb52b3a0e614
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/assistant_check_complete.ts
@@ -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 { loadAllActions as loadConnectors } from '@kbn/triggers-actions-ui-plugin/public/common/constants';
+import type { AIConnector } from '@kbn/elastic-assistant/impl/connectorland/connector_selector';
+import { i18n } from '@kbn/i18n';
+import type { OnboardingCardCheckComplete } from '../../../../types';
+import { AllowedActionTypeIds } from './constants';
+import type { AssistantCardMetadata } from './types';
+
+export const checkAssistantCardComplete: OnboardingCardCheckComplete<
+ AssistantCardMetadata
+> = async ({ http }) => {
+ const allConnectors = await loadConnectors({ http });
+
+ const aiConnectors = allConnectors.reduce((acc: AIConnector[], connector) => {
+ if (!connector.isMissingSecrets && AllowedActionTypeIds.includes(connector.actionTypeId)) {
+ acc.push(connector);
+ }
+ return acc;
+ }, []);
+
+ const completeBadgeText = i18n.translate(
+ 'xpack.securitySolution.onboarding.assistantCard.badge.completeText',
+ {
+ defaultMessage: '{count} AI {count, plural, one {connector} other {connectors}} added',
+ values: { count: aiConnectors.length },
+ }
+ );
+
+ return {
+ isComplete: aiConnectors.length > 0,
+ completeBadgeText,
+ metadata: {
+ connectors: aiConnectors,
+ },
+ };
+};
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/connectors/connector_cards.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/connectors/connector_cards.tsx
new file mode 100644
index 0000000000000..3cdefaa1fe490
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/connectors/connector_cards.tsx
@@ -0,0 +1,83 @@
+/*
+ * 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 AIConnector } from '@kbn/elastic-assistant/impl/connectorland/connector_selector';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiPanel,
+ EuiLoadingSpinner,
+ EuiText,
+ EuiBadge,
+ EuiSpacer,
+} from '@elastic/eui';
+import { css } from '@emotion/css';
+import { useKibana } from '../../../../../../common/lib/kibana';
+import { CreateConnectorPopover } from './create_connector_popover';
+import { ConnectorSetup } from './connector_setup';
+
+interface ConnectorCardsProps {
+ connectors?: AIConnector[];
+ onConnectorSaved: () => void;
+}
+
+export const ConnectorCards = React.memo(
+ ({ connectors, onConnectorSaved }) => {
+ const {
+ triggersActionsUi: { actionTypeRegistry },
+ } = useKibana().services;
+
+ if (!connectors) return ;
+
+ if (connectors.length > 0) {
+ return (
+ <>
+
+ {connectors.map((connector) => (
+
+
+
+
+ {connector.name}
+
+
+
+ {actionTypeRegistry.get(connector.actionTypeId).actionTypeTitle}
+
+
+
+
+
+ ))}
+
+
+
+ >
+ );
+ }
+
+ return ;
+ }
+);
+ConnectorCards.displayName = 'ConnectorCards';
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/connectors/connector_setup.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/connectors/connector_setup.tsx
new file mode 100644
index 0000000000000..f41c77458edf9
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/connectors/connector_setup.tsx
@@ -0,0 +1,147 @@
+/*
+ * 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, { useCallback, useState } from 'react';
+import {
+ useEuiTheme,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiListGroup,
+ EuiIcon,
+ EuiPanel,
+ EuiLoadingSpinner,
+ EuiText,
+ EuiLink,
+ EuiTextColor,
+} from '@elastic/eui';
+import { css } from '@emotion/css';
+import {
+ ConnectorAddModal,
+ type ActionConnector,
+} from '@kbn/triggers-actions-ui-plugin/public/common/constants';
+import type { ActionType } from '@kbn/actions-plugin/common';
+import { useKibana } from '../../../../../../common/lib/kibana';
+import { useFilteredActionTypes } from './hooks/use_load_action_types';
+
+const usePanelCss = () => {
+ const { euiTheme } = useEuiTheme();
+ return css`
+ .connectorSelectorPanel {
+ height: 160px;
+ &.euiPanel:hover {
+ background-color: ${euiTheme.colors.lightestShade};
+ }
+ }
+ `;
+};
+
+interface ConnectorSetupProps {
+ onConnectorSaved?: (savedAction: ActionConnector) => void;
+ onClose?: () => void;
+ compressed?: boolean;
+}
+export const ConnectorSetup = React.memo(
+ ({ onConnectorSaved, onClose, compressed = false }) => {
+ const panelCss = usePanelCss();
+ const {
+ http,
+ triggersActionsUi: { actionTypeRegistry },
+ notifications: { toasts },
+ } = useKibana().services;
+ const [selectedActionType, setSelectedActionType] = useState(null);
+
+ const onModalClose = useCallback(() => {
+ setSelectedActionType(null);
+ onClose?.();
+ }, [onClose]);
+
+ const actionTypes = useFilteredActionTypes(http, toasts);
+
+ if (!actionTypes) {
+ return ;
+ }
+
+ return (
+ <>
+ {compressed ? (
+ ({
+ key: actionType.id,
+ id: actionType.id,
+ label: actionType.name,
+ size: 's',
+ icon: (
+
+ ),
+ isDisabled: !actionType.enabled,
+ onClick: () => setSelectedActionType(actionType),
+ }))}
+ />
+ ) : (
+
+ {actionTypes.map((actionType: ActionType) => (
+
+ setSelectedActionType(actionType)}
+ data-test-subj={`actionType-${actionType.id}`}
+ className={panelCss}
+ >
+
+
+
+
+
+
+
+ {actionType.name}
+
+
+
+
+
+
+ ))}
+
+ )}
+
+ {selectedActionType && (
+
+ )}
+ >
+ );
+ }
+);
+ConnectorSetup.displayName = 'ConnectorSetup';
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/connectors/create_connector_popover.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/connectors/create_connector_popover.tsx
new file mode 100644
index 0000000000000..de432d6597afd
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/connectors/create_connector_popover.tsx
@@ -0,0 +1,48 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import React, { useCallback, useState } from 'react';
+import { css } from '@emotion/css';
+import { EuiPopover, EuiLink, EuiText } from '@elastic/eui';
+import { ConnectorSetup } from './connector_setup';
+import * as i18n from '../translations';
+
+interface CreateConnectorPopoverProps {
+ onConnectorSaved: () => void;
+}
+
+export const CreateConnectorPopover = React.memo(
+ ({ onConnectorSaved }) => {
+ const [isOpen, setIsPopoverOpen] = useState(false);
+ const closePopover = useCallback(() => setIsPopoverOpen(false), []);
+
+ const onButtonClick = useCallback(
+ () => setIsPopoverOpen((isPopoverOpen) => !isPopoverOpen),
+ []
+ );
+
+ return (
+
+
+ {i18n.ASSISTANT_CARD_CREATE_NEW_CONNECTOR_POPOVER}
+
+
+ }
+ isOpen={isOpen}
+ closePopover={closePopover}
+ data-test-subj="createConnectorPopover"
+ >
+
+
+ );
+ }
+);
+CreateConnectorPopover.displayName = 'CreateConnectorPopover';
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/connectors/hooks/use_load_action_types.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/connectors/hooks/use_load_action_types.ts
new file mode 100644
index 0000000000000..5bdee57baafc0
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/connectors/hooks/use_load_action_types.ts
@@ -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 { useMemo } from 'react';
+import { useLoadActionTypes as loadActionTypes } from '@kbn/elastic-assistant/impl/connectorland/use_load_action_types';
+import type { HttpSetup } from '@kbn/core-http-browser';
+import type { IToasts } from '@kbn/core-notifications-browser';
+import { AllowedActionTypeIds } from '../../constants';
+
+export const useFilteredActionTypes = (http: HttpSetup, toasts: IToasts) => {
+ const { data } = loadActionTypes({ http, toasts });
+ return useMemo(() => data?.filter(({ id }) => AllowedActionTypeIds.includes(id)), [data]);
+};
diff --git a/x-pack/plugins/security_solution/public/common/components/empty_prompt/constants.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/constants.ts
similarity index 76%
rename from x-pack/plugins/security_solution/public/common/components/empty_prompt/constants.ts
rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/constants.ts
index 0cf6b35ceab99..35811c18de471 100644
--- a/x-pack/plugins/security_solution/public/common/components/empty_prompt/constants.ts
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/constants.ts
@@ -5,4 +5,4 @@
* 2.0.
*/
-export const VIDEO_SOURCE = '//play.vidyard.com/K6kKDBbP9SpXife9s2tHNP.html?';
+export const AllowedActionTypeIds = ['.bedrock', '.gen-ai', '.gemini'];
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/index.ts
new file mode 100644
index 0000000000000..4110575ecc712
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/index.ts
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { AssistantAvatar } from '@kbn/elastic-assistant';
+import type { OnboardingCardConfig } from '../../../../types';
+import { OnboardingCardId } from '../../../../constants';
+import { ASSISTANT_CARD_TITLE } from './translations';
+import { checkAssistantCardComplete } from './assistant_check_complete';
+import type { AssistantCardMetadata } from './types';
+
+export const assistantCardConfig: OnboardingCardConfig = {
+ id: OnboardingCardId.assistant,
+ title: ASSISTANT_CARD_TITLE,
+ icon: AssistantAvatar,
+ Component: React.lazy(
+ () =>
+ import(
+ /* webpackChunkName: "onboarding_assistant_card" */
+ './assistant_card'
+ )
+ ),
+ checkComplete: checkAssistantCardComplete,
+};
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/translations.ts
new file mode 100644
index 0000000000000..41e73bdacf061
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/translations.ts
@@ -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 { i18n } from '@kbn/i18n';
+
+export const ASSISTANT_CARD_TITLE = i18n.translate(
+ 'xpack.securitySolution.onboarding.assistantCard.title',
+ {
+ defaultMessage: 'Configure AI Assistant',
+ }
+);
+
+export const ASSISTANT_CARD_DESCRIPTION = i18n.translate(
+ 'xpack.securitySolution.onboarding.assistantCard.description',
+ {
+ defaultMessage:
+ 'Choose and configure any AI provider available to use with Elastic AI Assistant.',
+ }
+);
+
+export const ASSISTANT_CARD_CALLOUT_INTEGRATIONS_TEXT = i18n.translate(
+ 'xpack.securitySolution.onboarding.assistantCard.calloutIntegrationsText',
+ {
+ defaultMessage: 'To add Elastic rules add integrations first.',
+ }
+);
+
+export const ASSISTANT_CARD_CALLOUT_INTEGRATIONS_BUTTON = i18n.translate(
+ 'xpack.securitySolution.onboarding.assistantCard.calloutIntegrationsButton',
+ {
+ defaultMessage: 'Add integrations step',
+ }
+);
+
+export const ASSISTANT_CARD_CREATE_NEW_CONNECTOR_POPOVER = i18n.translate(
+ 'xpack.securitySolution.onboarding.assistantCard.createNewConnectorPopover',
+ {
+ defaultMessage: 'Create new connector',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/types.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/types.ts
new file mode 100644
index 0000000000000..f1e0216406391
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/assistant/types.ts
@@ -0,0 +1,12 @@
+/*
+ * 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 { ActionConnector } from '@kbn/alerts-ui-shared';
+
+export interface AssistantCardMetadata {
+ connectors: ActionConnector[];
+}
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/attack_discover_card.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/attack_discover_card.test.tsx
new file mode 100644
index 0000000000000..19b327f77487c
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/attack_discover_card.test.tsx
@@ -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 React from 'react';
+import { render } from '@testing-library/react';
+import { AttackDiscoveryCard } from './attack_discovery_card';
+import { TestProviders } from '../../../../../common/mock';
+
+const props = {
+ setComplete: jest.fn(),
+ checkComplete: jest.fn(),
+ isCardComplete: jest.fn(),
+ setExpandedCardId: jest.fn(),
+};
+
+describe('RulesCard', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('description should be in the document', () => {
+ const { getByTestId } = render(
+
+
+
+ );
+
+ expect(getByTestId('attackDiscoveryCardDescription')).toBeInTheDocument();
+ });
+
+ it('card callout should be rendered if integrations cards is not complete', () => {
+ props.isCardComplete.mockReturnValueOnce(false);
+
+ const { getByText } = render(
+
+
+
+ );
+
+ expect(getByText('To use Attack Discovery add integrations first.')).toBeInTheDocument();
+ });
+
+ it('card button should be disabled if integrations cards is not complete', () => {
+ props.isCardComplete.mockReturnValueOnce(false);
+
+ const { getByTestId } = render(
+
+
+
+ );
+
+ expect(getByTestId('attackDiscoveryCardButton').querySelector('button')).toBeDisabled();
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/attack_discovery_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/attack_discovery_card.tsx
new file mode 100644
index 0000000000000..31c98bbea097b
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/attack_discovery_card.tsx
@@ -0,0 +1,86 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useCallback, useMemo } from 'react';
+import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
+import { SecurityPageName } from '@kbn/security-solution-navigation';
+import { SecuritySolutionLinkButton } from '../../../../../common/components/links';
+import { OnboardingCardId } from '../../../../constants';
+import type { OnboardingCardComponent } from '../../../../types';
+import { OnboardingCardContentImagePanel } from '../common/card_content_image_panel';
+import { CardCallOut } from '../common/card_callout';
+import attackDiscoveryImageSrc from './images/attack_discovery.png';
+import * as i18n from './translations';
+
+export const AttackDiscoveryCard: OnboardingCardComponent = React.memo(
+ ({ isCardComplete, setExpandedCardId, setComplete }) => {
+ const isIntegrationsCardComplete = useMemo(
+ () => isCardComplete(OnboardingCardId.integrations),
+ [isCardComplete]
+ );
+
+ const expandIntegrationsCard = useCallback(() => {
+ setExpandedCardId(OnboardingCardId.integrations, { scroll: true });
+ }, [setExpandedCardId]);
+
+ return (
+
+
+
+
+ {i18n.ATTACK_DISCOVERY_CARD_DESCRIPTION}
+
+ {!isIntegrationsCardComplete && (
+ <>
+
+
+
+
+ {i18n.ATTACK_DISCOVERY_CARD_CALLOUT_INTEGRATIONS_BUTTON}
+
+
+
+
+
+
+ }
+ />
+ >
+ )}
+
+
+ setComplete(true)}
+ deepLinkId={SecurityPageName.attackDiscovery}
+ fill
+ isDisabled={!isIntegrationsCardComplete}
+ >
+ {i18n.ATTACK_DISCOVERY_CARD_START_ATTACK_DISCOVERY_BUTTON}
+
+
+
+
+ );
+ }
+);
+AttackDiscoveryCard.displayName = 'AttackDiscoveryCard';
+
+// eslint-disable-next-line import/no-default-export
+export default AttackDiscoveryCard;
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/images/analyze_data_using_dashboards.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/images/attack_discovery.png
similarity index 100%
rename from x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/images/analyze_data_using_dashboards.png
rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/images/attack_discovery.png
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/images/attack_discovery_icon.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/images/attack_discovery_icon.png
new file mode 100644
index 0000000000000..912b0cae64733
Binary files /dev/null and b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/images/attack_discovery_icon.png differ
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/index.ts
new file mode 100644
index 0000000000000..827fbf25cf8ce
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/index.ts
@@ -0,0 +1,25 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import type { OnboardingCardConfig } from '../../../../types';
+import { OnboardingCardId } from '../../../../constants';
+import { ATTACK_DISCOVERY_CARD_TITLE } from './translations';
+import attackDiscoveryIcon from './images/attack_discovery_icon.png';
+
+export const attackDiscoveryCardConfig: OnboardingCardConfig = {
+ id: OnboardingCardId.attackDiscovery,
+ title: ATTACK_DISCOVERY_CARD_TITLE,
+ icon: attackDiscoveryIcon,
+ Component: React.lazy(
+ () =>
+ import(
+ /* webpackChunkName: "onboarding_attack_discovery_card" */
+ './attack_discovery_card'
+ )
+ ),
+};
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/translations.ts
new file mode 100644
index 0000000000000..be1334b35b217
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/attack_discovery/translations.ts
@@ -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 { i18n } from '@kbn/i18n';
+
+export const ATTACK_DISCOVERY_CARD_TITLE = i18n.translate(
+ 'xpack.securitySolution.onboarding.attackDiscoveryCard.title',
+ {
+ defaultMessage: 'Start using Attack Discovery',
+ }
+);
+
+export const ATTACK_DISCOVERY_CARD_DESCRIPTION = i18n.translate(
+ 'xpack.securitySolution.onboarding.attackDiscoveryCard.description',
+ {
+ defaultMessage:
+ 'Visualize, sort, filter, and investigate alerts from across your infrastructure. Examine individual alerts of interest, and discover general patterns in alert volume and severity.',
+ }
+);
+
+export const ATTACK_DISCOVERY_CARD_CALLOUT_INTEGRATIONS_TEXT = i18n.translate(
+ 'xpack.securitySolution.onboarding.attackDiscoveryCard.calloutIntegrationsText',
+ {
+ defaultMessage: 'To use Attack Discovery add integrations first.',
+ }
+);
+
+export const ATTACK_DISCOVERY_CARD_CALLOUT_INTEGRATIONS_BUTTON = i18n.translate(
+ 'xpack.securitySolution.onboarding.attackDiscoveryCard.calloutIntegrationsButton',
+ {
+ defaultMessage: 'Add integrations step',
+ }
+);
+
+export const ATTACK_DISCOVERY_CARD_START_ATTACK_DISCOVERY_BUTTON = i18n.translate(
+ 'xpack.securitySolution.onboarding.attackDiscoveryCard.startAttackDiscoveryButton',
+ {
+ defaultMessage: 'Start using Attack Discovery',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/styles/progress_bar.style.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_callout.styles.ts
similarity index 52%
rename from x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/styles/progress_bar.style.ts
rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_callout.styles.ts
index 466d4dfaa8dc8..ffa772044896e 100644
--- a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/styles/progress_bar.style.ts
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_callout.styles.ts
@@ -5,21 +5,14 @@
* 2.0.
*/
-import { useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/css';
-import { useMemo } from 'react';
+import { useEuiTheme } from '@elastic/eui';
-export const useProgressBarStyles = () => {
+export const useCardCallOutStyles = () => {
const { euiTheme } = useEuiTheme();
- const progressBarStyles = useMemo(
- () => ({
- textStyle: css({
- fontSize: '10.5px',
- fontWeight: euiTheme.font.weight.bold,
- textTransform: 'uppercase',
- }),
- }),
- [euiTheme.font.weight.bold]
- );
- return progressBarStyles;
+ return css`
+ padding: ${euiTheme.size.s};
+ border: 1px solid ${euiTheme.colors.lightShade};
+ border-radius: ${euiTheme.size.s};
+ `;
};
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_callout.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_callout.tsx
new file mode 100644
index 0000000000000..b53952d24d3b6
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_callout.tsx
@@ -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 React from 'react';
+import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui';
+import type { EuiCallOutProps, IconType } from '@elastic/eui';
+import { useCardCallOutStyles } from './card_callout.styles';
+
+export interface CardCallOutProps {
+ text: React.ReactNode;
+ color?: EuiCallOutProps['color'];
+ icon?: IconType;
+ action?: React.ReactNode;
+}
+
+export const CardCallOut = React.memo(({ text, color, icon, action }) => {
+ const styles = useCardCallOutStyles();
+ return (
+
+
+
+
+ {icon && (
+
+
+
+ )}
+
+ {text}
+
+
+
+ {action && (
+
+ {action}
+
+ )}
+
+
+ );
+});
+CardCallOut.displayName = 'CardCallOut';
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_image_panel.styles.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_image_panel.styles.ts
new file mode 100644
index 0000000000000..d8f6d6c278ee3
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_image_panel.styles.ts
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { css } from '@emotion/css';
+import { useEuiTheme, useEuiShadow } from '@elastic/eui';
+
+export const useCardContentImagePanelStyles = () => {
+ const { euiTheme } = useEuiTheme();
+ const shadowStyles = useEuiShadow('m');
+ return css`
+ padding-top: 8px;
+ .cardSpacer {
+ width: 8%;
+ }
+ .cardImage {
+ width: 50%;
+ img {
+ width: 100%;
+ border-radius: ${euiTheme.size.s};
+ ${shadowStyles}
+ }
+ }
+ `;
+};
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_image_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_image_panel.tsx
new file mode 100644
index 0000000000000..2b867969beb95
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_image_panel.tsx
@@ -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, { type PropsWithChildren } from 'react';
+import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
+import { OnboardingCardContentPanel } from './card_content_panel';
+import { useCardContentImagePanelStyles } from './card_content_image_panel.styles';
+
+export const OnboardingCardContentImagePanel = React.memo<
+ PropsWithChildren<{ imageSrc: string; imageAlt: string }>
+>(({ children, imageSrc, imageAlt }) => {
+ const styles = useCardContentImagePanelStyles();
+ return (
+
+
+ {children}
+
+
+
+
+
+
+
+
+ );
+});
+OnboardingCardContentImagePanel.displayName = 'OnboardingCardContentImagePanel';
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.tsx
new file mode 100644
index 0000000000000..3d5489b9be1cc
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_content_panel.tsx
@@ -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 React, { type PropsWithChildren } from 'react';
+import { COLOR_MODES_STANDARD, EuiPanel, useEuiTheme, type EuiPanelProps } from '@elastic/eui';
+import { css } from '@emotion/react';
+
+export const OnboardingCardContentPanel = React.memo>(
+ ({ children, ...panelProps }) => {
+ const { euiTheme, colorMode } = useEuiTheme();
+ const isDarkMode = colorMode === COLOR_MODES_STANDARD.dark;
+ return (
+
+
+ {children}
+
+
+ );
+ }
+);
+OnboardingCardContentPanel.displayName = 'OnboardingCardContentWrapper';
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_link_button.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_link_button.tsx
new file mode 100644
index 0000000000000..96466466ee4a8
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/common/card_link_button.tsx
@@ -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 React, { useCallback } from 'react';
+import type { OnboardingCardId } from '../../../../constants';
+import { SecuritySolutionLinkButton } from '../../../../../common/components/links';
+import { useOnboardingContext } from '../../../onboarding_context';
+
+export interface CardLinkButtonProps {
+ linkId: string;
+ cardId: OnboardingCardId;
+}
+
+export const withReportCardLinkClick = (
+ WrappedComponent: React.ComponentType
+): React.FC =>
+ React.memo(function WithReportCardLinkClick({ onClick, cardId, linkId, ...rest }) {
+ const { reportCardLinkClicked } = useOnboardingContext();
+ const onClickWithReport = useCallback(
+ (ev) => {
+ reportCardLinkClicked(cardId, linkId);
+ onClick?.(ev);
+ },
+ [reportCardLinkClicked, cardId, linkId, onClick]
+ );
+ return ;
+ });
+
+export const CardLinkButton = withReportCardLinkClick(SecuritySolutionLinkButton);
+CardLinkButton.displayName = 'CardLinkButton';
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/dashboards_card.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/dashboards_card.test.tsx
new file mode 100644
index 0000000000000..f7aa198eccab4
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/dashboards_card.test.tsx
@@ -0,0 +1,76 @@
+/*
+ * 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 { fireEvent, render } from '@testing-library/react';
+import { DashboardsCard } from './dashboards_card';
+import { TestProviders } from '../../../../../common/mock';
+import { OnboardingCardId } from '../../../../constants';
+
+jest.mock('../../../onboarding_context');
+
+const props = {
+ setComplete: jest.fn(),
+ checkComplete: jest.fn(),
+ isCardComplete: jest.fn(),
+ setExpandedCardId: jest.fn(),
+};
+
+describe('RulesCard', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('description should be in the document', () => {
+ const { getByTestId } = render(
+
+
+
+ );
+
+ expect(getByTestId('dashboardsDescription')).toBeInTheDocument();
+ });
+
+ it('card callout should be rendered if integrations cards is not complete', () => {
+ props.isCardComplete.mockReturnValueOnce(false);
+
+ const { getByText } = render(
+
+
+
+ );
+
+ expect(getByText('To view dashboards add integrations first.')).toBeInTheDocument();
+ });
+
+ it('card button should be disabled if integrations cards is not complete', () => {
+ props.isCardComplete.mockReturnValueOnce(false);
+
+ const { getByTestId } = render(
+
+
+
+ );
+
+ expect(getByTestId('dashboardsCardButton').querySelector('button')).toBeDisabled();
+ });
+ it('should expand integrations card when callout link is clicked', () => {
+ props.isCardComplete.mockReturnValueOnce(false); // To show the callout
+
+ const { getByTestId } = render(
+
+
+
+ );
+
+ const link = getByTestId('dashboardsCardCalloutLink');
+ fireEvent.click(link);
+
+ expect(props.setExpandedCardId).toHaveBeenCalledWith(OnboardingCardId.integrations, {
+ scroll: true,
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/dashboards_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/dashboards_card.tsx
new file mode 100644
index 0000000000000..df98800d83f32
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/dashboards_card.tsx
@@ -0,0 +1,90 @@
+/*
+ * 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, { useCallback, useMemo } from 'react';
+import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
+import { SecurityPageName } from '@kbn/security-solution-navigation';
+import { OnboardingCardId } from '../../../../constants';
+import type { OnboardingCardComponent } from '../../../../types';
+import { OnboardingCardContentImagePanel } from '../common/card_content_image_panel';
+import { CardCallOut } from '../common/card_callout';
+import { CardLinkButton } from '../common/card_link_button';
+import dashboardsImageSrc from './images/dashboards.png';
+import * as i18n from './translations';
+
+export const DashboardsCard: OnboardingCardComponent = ({
+ isCardComplete,
+ setComplete,
+ setExpandedCardId,
+}) => {
+ const isIntegrationsCardComplete = useMemo(
+ () => isCardComplete(OnboardingCardId.integrations),
+ [isCardComplete]
+ );
+
+ const expandIntegrationsCard = useCallback(() => {
+ setExpandedCardId(OnboardingCardId.integrations, { scroll: true });
+ }, [setExpandedCardId]);
+
+ return (
+
+
+
+
+ {i18n.DASHBOARDS_CARD_DESCRIPTION}
+
+ {!isIntegrationsCardComplete && (
+ <>
+
+
+
+ {i18n.DASHBOARDS_CARD_CALLOUT_INTEGRATIONS_BUTTON}
+
+
+
+
+
+ }
+ />
+ >
+ )}
+
+
+ setComplete(true)}
+ linkId="goToDashboardsButton"
+ cardId={OnboardingCardId.dashboards}
+ deepLinkId={SecurityPageName.dashboards}
+ fill
+ isDisabled={!isIntegrationsCardComplete}
+ >
+ {i18n.DASHBOARDS_CARD_GO_TO_DASHBOARDS_BUTTON}
+
+
+
+
+ );
+};
+
+// eslint-disable-next-line import/no-default-export
+export default DashboardsCard;
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/images/dashboards.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/images/dashboards.png
new file mode 100644
index 0000000000000..0d6b551e09661
Binary files /dev/null and b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/images/dashboards.png differ
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/images/dashboards_icon.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/images/dashboards_icon.png
new file mode 100644
index 0000000000000..ddc024696e224
Binary files /dev/null and b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/images/dashboards_icon.png differ
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/index.ts
new file mode 100644
index 0000000000000..356b15f50bf9b
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/index.ts
@@ -0,0 +1,26 @@
+/*
+ * 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 { OnboardingCardConfig } from '../../../../types';
+import { OnboardingCardId } from '../../../../constants';
+import { DASHBOARDS_CARD_TITLE } from './translations';
+import dashboardsIcon from './images/dashboards_icon.png';
+
+export const dashboardsCardConfig: OnboardingCardConfig = {
+ id: OnboardingCardId.dashboards,
+ title: DASHBOARDS_CARD_TITLE,
+ icon: dashboardsIcon,
+ Component: React.lazy(
+ () =>
+ import(
+ /* webpackChunkName: "onboarding_dashboards_card" */
+ './dashboards_card'
+ )
+ ),
+ capabilities: ['dashboard.show'],
+};
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/translations.ts
new file mode 100644
index 0000000000000..cf1a280122d79
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/dashboards/translations.ts
@@ -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 { i18n } from '@kbn/i18n';
+
+export const DASHBOARDS_CARD_TITLE = i18n.translate(
+ 'xpack.securitySolution.onboarding.dashboardsCard.title',
+ {
+ defaultMessage: 'View and analyze your data using dashboards',
+ }
+);
+
+export const DASHBOARDS_CARD_DESCRIPTION = i18n.translate(
+ 'xpack.securitySolution.onboarding.dashboardsCard.description',
+ {
+ defaultMessage:
+ "Use dashboards to visualize data and stay up-to-date with key information. Create your own, or use Elastic's default dashboards — including alerts, user authentication events, known vulnerabilities, and more.",
+ }
+);
+export const DASHBOARDS_CARD_CALLOUT_INTEGRATIONS_TEXT = i18n.translate(
+ 'xpack.securitySolution.onboarding.dashboardsCard.calloutIntegrationsText',
+ {
+ defaultMessage: 'To view dashboards add integrations first.',
+ }
+);
+export const DASHBOARDS_CARD_CALLOUT_INTEGRATIONS_BUTTON = i18n.translate(
+ 'xpack.securitySolution.onboarding.dashboardsCard.calloutIntegrationsButton',
+ {
+ defaultMessage: 'Add integrations step',
+ }
+);
+export const DASHBOARDS_CARD_GO_TO_DASHBOARDS_BUTTON = i18n.translate(
+ 'xpack.securitySolution.onboarding.dashboardsCard.goToDashboardsButton',
+ {
+ defaultMessage: 'Go to dashboards',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/card_step/content/__mocks__/content_wrapper.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/__mocks__/integration_card_grid_tabs.tsx
similarity index 74%
rename from x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/card_step/content/__mocks__/content_wrapper.tsx
rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/__mocks__/integration_card_grid_tabs.tsx
index 442ef3a598bd7..660d7b881e397 100644
--- a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/card_step/content/__mocks__/content_wrapper.tsx
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/__mocks__/integration_card_grid_tabs.tsx
@@ -7,4 +7,4 @@
import React from 'react';
-export const ContentWrapper = ({ children }: { children: React.ReactElement }) => <>{children}>;
+export const IntegrationsCardGridTabs = () => ;
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/__mocks__/onboarding.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/__mocks__/package_list_grid.tsx
similarity index 78%
rename from x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/__mocks__/onboarding.tsx
rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/__mocks__/package_list_grid.tsx
index d541ab1a75c06..759dbf78bfb88 100644
--- a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/__mocks__/onboarding.tsx
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/__mocks__/package_list_grid.tsx
@@ -4,7 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
import React from 'react';
-export const Onboarding = () => ;
+export const PackageListGrid = () => ;
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/__mocks__/agent_required_callout.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/__mocks__/agent_required_callout.tsx
new file mode 100644
index 0000000000000..9a8fb5c014169
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/__mocks__/agent_required_callout.tsx
@@ -0,0 +1,10 @@
+/*
+ * 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';
+
+export const AgentRequiredCallout = () => ;
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/__mocks__/agentless_available_callout.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/__mocks__/agentless_available_callout.tsx
new file mode 100644
index 0000000000000..2f7ad32e5fc8c
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/__mocks__/agentless_available_callout.tsx
@@ -0,0 +1,10 @@
+/*
+ * 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';
+
+export const AgentlessAvailableCallout = () => ;
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/__mocks__/index.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/__mocks__/endpoint_callout.tsx
similarity index 76%
rename from x-pack/plugins/security_solution/public/common/components/landing_page/__mocks__/index.tsx
rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/__mocks__/endpoint_callout.tsx
index bee0fdfe52bf4..c2bdfdc72ea10 100644
--- a/x-pack/plugins/security_solution/public/common/components/landing_page/__mocks__/index.tsx
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/__mocks__/endpoint_callout.tsx
@@ -4,7 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-
import React from 'react';
-export const LandingPageComponent = () => ;
+export const EndpointCallout = () => ;
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/__mocks__/onboarding_with_settings.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/__mocks__/installed_integrations_callout.tsx
similarity index 72%
rename from x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/__mocks__/onboarding_with_settings.tsx
rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/__mocks__/installed_integrations_callout.tsx
index da354ec8edcc2..eabc4446bcc77 100644
--- a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/__mocks__/onboarding_with_settings.tsx
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/__mocks__/installed_integrations_callout.tsx
@@ -7,6 +7,6 @@
import React from 'react';
-export const OnboardingWithSettingsComponent = () => (
-
+export const InstalledIntegrationsCallout = () => (
+
);
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/__mocks__/integration_card_top_callout.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/__mocks__/integration_card_top_callout.tsx
new file mode 100644
index 0000000000000..c51593181b33e
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/__mocks__/integration_card_top_callout.tsx
@@ -0,0 +1,11 @@
+/*
+ * 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';
+
+export const IntegrationCardTopCallout = jest.fn(() => (
+
+));
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/__mocks__/index.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/__mocks__/manage_integrations_callout.tsx
similarity index 71%
rename from x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/__mocks__/index.tsx
rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/__mocks__/manage_integrations_callout.tsx
index 4491d62637144..828a49ab69c07 100644
--- a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/__mocks__/index.tsx
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/__mocks__/manage_integrations_callout.tsx
@@ -6,6 +6,4 @@
*/
import React from 'react';
-export const Onboarding = jest
- .fn()
- .mockReturnValue();
+export const ManageIntegrationsCallout = () => ;
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agent_required_callout.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agent_required_callout.test.tsx
new file mode 100644
index 0000000000000..dbd0c105d27a1
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agent_required_callout.test.tsx
@@ -0,0 +1,33 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+/*
+ * 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 { render } from '@testing-library/react';
+import { AgentRequiredCallout } from './agent_required_callout';
+import { TestProviders } from '../../../../../../common/mock/test_providers';
+
+jest.mock('../../../../../../common/lib/kibana');
+
+describe('AgentRequiredCallout', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders the warning callout when an agent is still required', () => {
+ const { getByTestId, getByText } = render(, { wrapper: TestProviders });
+
+ expect(
+ getByText('Elastic Agent is required for one or more of your integrations. Add Elastic Agent')
+ ).toBeInTheDocument();
+ expect(getByTestId('agentLink')).toBeInTheDocument();
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agent_required_callout.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agent_required_callout.tsx
new file mode 100644
index 0000000000000..aad22c959bc65
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agent_required_callout.tsx
@@ -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 React, { useCallback } from 'react';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { EuiIcon } from '@elastic/eui';
+
+import { LinkAnchor } from '../../../../../../common/components/links';
+import { CardCallOut } from '../../common/card_callout';
+import { useNavigation } from '../../../../../../common/lib/kibana';
+import { FLEET_APP_ID, ADD_AGENT_PATH } from '../constants';
+
+const fleetAgentLinkProps = { appId: FLEET_APP_ID, path: ADD_AGENT_PATH };
+
+export const AgentRequiredCallout = React.memo(() => {
+ const { getAppUrl, navigateTo } = useNavigation();
+ const addAgentLink = getAppUrl(fleetAgentLinkProps);
+ const onAddAgentClick = useCallback(() => {
+ navigateTo(fleetAgentLinkProps);
+ }, [navigateTo]);
+
+ return (
+
+ ),
+ link: (
+
+
+
+ ),
+ icon: ,
+ }}
+ />
+ }
+ />
+ );
+});
+
+AgentRequiredCallout.displayName = 'AgentRequiredCallout';
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agentless_available_callout.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agentless_available_callout.test.tsx
new file mode 100644
index 0000000000000..48939cc3db60f
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agentless_available_callout.test.tsx
@@ -0,0 +1,65 @@
+/*
+ * 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 { render } from '@testing-library/react';
+import React from 'react';
+import { TestProviders } from '../../../../../../common/mock/test_providers';
+import { AgentlessAvailableCallout } from './agentless_available_callout';
+import { useKibana } from '../../../../../../common/lib/kibana';
+
+jest.mock('../../../../../../common/lib/kibana', () => ({
+ useKibana: jest.fn(),
+}));
+
+describe('AgentlessAvailableCallout', () => {
+ const mockUseKibana = useKibana as jest.Mock;
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockUseKibana.mockReturnValue({
+ services: {
+ docLinks: {
+ links: {
+ fleet: {
+ agentlessBlog: 'https://www.elastic.co/blog',
+ },
+ },
+ },
+ },
+ });
+ });
+
+ it('returns null if agentlessBlog is null', () => {
+ mockUseKibana.mockReturnValue({
+ services: {
+ docLinks: {
+ links: {
+ fleet: {
+ agentlessBlog: null,
+ },
+ },
+ },
+ },
+ });
+ const { container } = render(, {
+ wrapper: TestProviders,
+ });
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it('renders the agentless available text', () => {
+ const { getByText, getByTestId } = render(, {
+ wrapper: TestProviders,
+ });
+ expect(getByText('NEW')).toBeInTheDocument();
+ expect(
+ getByText(
+ 'Identify configuration risks in your cloud account with new and simplified agentless configuration'
+ )
+ ).toBeInTheDocument();
+ expect(getByTestId('agentlessLearnMoreLink')).toBeInTheDocument();
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agentless_available_callout.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agentless_available_callout.tsx
new file mode 100644
index 0000000000000..f802f83efb7e5
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/agentless_available_callout.tsx
@@ -0,0 +1,74 @@
+/*
+ * 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 { FormattedMessage } from '@kbn/i18n-react';
+import { EuiIcon, useEuiTheme } from '@elastic/eui';
+import { css } from '@emotion/react';
+
+import { useKibana } from '../../../../../../common/lib/kibana';
+import { LinkAnchor } from '../../../../../../common/components/links';
+import { CardCallOut } from '../../common/card_callout';
+
+export const AgentlessAvailableCallout = React.memo(() => {
+ const { euiTheme } = useEuiTheme();
+ const { docLinks } = useKibana().services;
+
+ /* @ts-expect-error: add the blog link to `packages/kbn-doc-links/src/get_doc_links.ts` when it is ready and remove this exit condition*/
+ if (!docLinks.links.fleet.agentlessBlog) {
+ return null;
+ }
+
+ return (
+ ,
+ new: (
+
+
+
+ ),
+ text: (
+
+ ),
+ link: (
+
+
+
+ ),
+ }}
+ />
+ }
+ />
+ );
+});
+
+AgentlessAvailableCallout.displayName = 'AgentlessAvailableCallout';
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/endpoint_callout.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/endpoint_callout.tsx
new file mode 100644
index 0000000000000..2ff48a1992d1d
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/endpoint_callout.tsx
@@ -0,0 +1,68 @@
+/*
+ * 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 { FormattedMessage } from '@kbn/i18n-react';
+import { EuiIcon, useEuiTheme } from '@elastic/eui';
+import { css } from '@emotion/react';
+
+import { useKibana } from '../../../../../../common/lib/kibana/kibana_react';
+import { LinkAnchor } from '../../../../../../common/components/links';
+import { CardCallOut } from '../../common/card_callout';
+
+export const EndpointCallout = React.memo(() => {
+ const { euiTheme } = useEuiTheme();
+ const { docLinks } = useKibana().services;
+
+ return (
+ ,
+ new: (
+
+
+
+ ),
+ text: (
+
+ ),
+ link: (
+
+
+
+ ),
+ }}
+ />
+ }
+ />
+ );
+});
+
+EndpointCallout.displayName = 'EndpointCallout';
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/installed_integrations_callout.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/installed_integrations_callout.test.tsx
new file mode 100644
index 0000000000000..3c47c24fd63ec
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/installed_integrations_callout.test.tsx
@@ -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 React from 'react';
+import { render } from '@testing-library/react';
+import { InstalledIntegrationsCallout } from './installed_integrations_callout';
+jest.mock('./agent_required_callout');
+jest.mock('./manage_integrations_callout');
+
+describe('InstalledIntegrationsCallout', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders the callout and available packages when integrations are installed', () => {
+ const mockMetadata = {
+ installedIntegrationsCount: 3,
+ isAgentRequired: false,
+ };
+
+ const { getByTestId } = render();
+
+ expect(getByTestId('manageIntegrationsCallout')).toBeInTheDocument();
+ });
+
+ it('renders the warning callout when an agent is still required', () => {
+ const mockMetadata = {
+ installedIntegrationsCount: 2,
+ isAgentRequired: true,
+ };
+
+ const { getByTestId } = render();
+
+ expect(getByTestId('agentRequiredCallout')).toBeInTheDocument();
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/installed_integrations_callout.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/installed_integrations_callout.tsx
new file mode 100644
index 0000000000000..6a82a538e39ad
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/installed_integrations_callout.tsx
@@ -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 React from 'react';
+
+import { AgentRequiredCallout } from './agent_required_callout';
+import { ManageIntegrationsCallout } from './manage_integrations_callout';
+
+export const InstalledIntegrationsCallout = React.memo(
+ ({
+ installedIntegrationsCount,
+ isAgentRequired,
+ }: {
+ installedIntegrationsCount: number;
+ isAgentRequired: boolean;
+ }) => {
+ if (!installedIntegrationsCount) {
+ return null;
+ }
+
+ return isAgentRequired ? (
+
+ ) : (
+
+ );
+ }
+);
+
+InstalledIntegrationsCallout.displayName = 'InstalledIntegrationsCallout';
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/integration_card_top_callout.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/integration_card_top_callout.test.tsx
new file mode 100644
index 0000000000000..e0aedafe45595
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/integration_card_top_callout.test.tsx
@@ -0,0 +1,81 @@
+/*
+ * 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 { render, waitFor } from '@testing-library/react';
+import { of } from 'rxjs';
+import { IntegrationCardTopCallout } from './integration_card_top_callout';
+import { useOnboardingService } from '../../../../../hooks/use_onboarding_service';
+import { IntegrationTabId } from '../types';
+
+jest.mock('../../../../../hooks/use_onboarding_service', () => ({
+ useOnboardingService: jest.fn(),
+}));
+
+jest.mock('./agentless_available_callout');
+jest.mock('./installed_integrations_callout');
+jest.mock('./endpoint_callout');
+
+describe('IntegrationCardTopCallout', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('renders EndpointCallout when endpoint tab selected and no integrations installed', async () => {
+ (useOnboardingService as jest.Mock).mockReturnValue({
+ isAgentlessAvailable$: of(true),
+ });
+
+ const { getByTestId } = render(
+
+ );
+
+ await waitFor(() => {
+ expect(getByTestId('endpointCallout')).toBeInTheDocument();
+ });
+ });
+
+ test('renders AgentlessAvailableCallout when agentless is available and no integrations installed', async () => {
+ (useOnboardingService as jest.Mock).mockReturnValue({
+ isAgentlessAvailable$: of(true),
+ });
+
+ const { getByTestId } = render(
+
+ );
+
+ await waitFor(() => {
+ expect(getByTestId('agentlessAvailableCallout')).toBeInTheDocument();
+ });
+ });
+
+ test('renders InstalledIntegrationsCallout when there are installed integrations', async () => {
+ (useOnboardingService as jest.Mock).mockReturnValue({
+ isAgentlessAvailable$: of(false),
+ });
+
+ const { getByTestId } = render(
+
+ );
+
+ await waitFor(() => {
+ expect(getByTestId('installedIntegrationsCallout')).toBeInTheDocument();
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/integration_card_top_callout.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/integration_card_top_callout.tsx
new file mode 100644
index 0000000000000..27c92d0f0b11f
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/integration_card_top_callout.tsx
@@ -0,0 +1,53 @@
+/*
+ * 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 { useObservable } from 'react-use';
+
+import { useOnboardingService } from '../../../../../hooks/use_onboarding_service';
+import { AgentlessAvailableCallout } from './agentless_available_callout';
+import { InstalledIntegrationsCallout } from './installed_integrations_callout';
+import { IntegrationTabId } from '../types';
+import { EndpointCallout } from './endpoint_callout';
+
+export const IntegrationCardTopCallout = React.memo(
+ ({
+ installedIntegrationsCount,
+ isAgentRequired,
+ selectedTabId,
+ }: {
+ installedIntegrationsCount: number;
+ isAgentRequired: boolean;
+ selectedTabId: IntegrationTabId;
+ }) => {
+ const { isAgentlessAvailable$ } = useOnboardingService();
+ const isAgentlessAvailable = useObservable(isAgentlessAvailable$, undefined);
+
+ const showAgentlessCallout =
+ isAgentlessAvailable &&
+ installedIntegrationsCount === 0 &&
+ selectedTabId !== IntegrationTabId.endpoint;
+ const showEndpointCallout =
+ installedIntegrationsCount === 0 && selectedTabId === IntegrationTabId.endpoint;
+ const showInstalledCallout = installedIntegrationsCount > 0 || isAgentRequired;
+
+ return (
+ <>
+ {showEndpointCallout && }
+ {showAgentlessCallout && }
+ {showInstalledCallout && (
+
+ )}
+ >
+ );
+ }
+);
+
+IntegrationCardTopCallout.displayName = 'IntegrationCardTopCallout';
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/manage_integrations_callout.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/manage_integrations_callout.test.tsx
new file mode 100644
index 0000000000000..5f16bf3981f5f
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/manage_integrations_callout.test.tsx
@@ -0,0 +1,48 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import React from 'react';
+import { render } from '@testing-library/react';
+import { ManageIntegrationsCallout } from './manage_integrations_callout';
+import { TestProviders } from '../../../../../../common/mock/test_providers';
+
+jest.mock('../../../../../../common/hooks/use_add_integrations_url', () => ({
+ useAddIntegrationsUrl: jest.fn().mockReturnValue({
+ href: '/test-url',
+ onClick: jest.fn(),
+ }),
+}));
+
+jest.mock('../../common/card_callout', () => ({
+ CardCallOut: ({ text }: { text: React.ReactNode }) => {text}
,
+}));
+
+describe('ManageIntegrationsCallout', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('renders nothing when installedIntegrationsCount is 0', () => {
+ const { queryByTestId } = render(, {
+ wrapper: TestProviders,
+ });
+
+ expect(queryByTestId('integrationsCompleteText')).not.toBeInTheDocument();
+ });
+
+ test('renders callout with correct message and link when there are installed integrations', () => {
+ const { getByText, getByTestId } = render(
+ ,
+ {
+ wrapper: TestProviders,
+ }
+ );
+
+ expect(getByText('5 integrations have been added')).toBeInTheDocument();
+ expect(getByTestId('manageIntegrationsLink')).toHaveTextContent('Manage integrations');
+ expect(getByTestId('manageIntegrationsLink')).toHaveAttribute('href', '/test-url');
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/manage_integrations_callout.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/manage_integrations_callout.tsx
new file mode 100644
index 0000000000000..3a052d927ff10
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/callouts/manage_integrations_callout.tsx
@@ -0,0 +1,63 @@
+/*
+ * 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 { FormattedMessage } from '@kbn/i18n-react';
+import { EuiIcon } from '@elastic/eui';
+
+import { LinkAnchor } from '../../../../../../common/components/links';
+import { CardCallOut } from '../../common/card_callout';
+import { useAddIntegrationsUrl } from '../../../../../../common/hooks/use_add_integrations_url';
+
+export const ManageIntegrationsCallout = React.memo(
+ ({ installedIntegrationsCount }: { installedIntegrationsCount: number }) => {
+ const { href: integrationUrl, onClick: onAddIntegrationClicked } = useAddIntegrationsUrl();
+
+ if (!installedIntegrationsCount) {
+ return null;
+ }
+
+ return (
+
+ ),
+ link: (
+
+
+
+ ),
+ icon: ,
+ }}
+ />
+ }
+ />
+ );
+ }
+);
+
+ManageIntegrationsCallout.displayName = 'ManageIntegrationsCallout';
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/constants.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/constants.ts
new file mode 100644
index 0000000000000..e245de6129478
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/constants.ts
@@ -0,0 +1,26 @@
+/*
+ * 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 { CategoryFacet } from '@kbn/fleet-plugin/public';
+import { INTEGRATION_TABS } from './integration_tabs_configs';
+import type { Tab } from './types';
+
+export const ADD_AGENT_PATH = `/agents`;
+export const AGENT_INDEX = `logs-elastic_agent*`;
+export const CARD_DESCRIPTION_LINE_CLAMP = 3; // 3 lines of text
+export const CARD_TITLE_LINE_CLAMP = 1; // 1 line of text
+export const DEFAULT_TAB: Tab = INTEGRATION_TABS[0];
+export const FLEET_APP_ID = `fleet`;
+export const INTEGRATION_APP_ID = `integrations`;
+export const LOADING_SKELETON_TEXT_LINES = 10; // 10 lines of text
+export const MAX_CARD_HEIGHT_IN_PX = 127; // px
+export const ONBOARDING_APP_ID = 'onboardingAppId';
+export const ONBOARDING_LINK = 'onboardingLink';
+export const SCROLL_ELEMENT_ID = 'integrations-scroll-container';
+export const SEARCH_FILTER_CATEGORIES: CategoryFacet[] = [];
+export const WITH_SEARCH_BOX_HEIGHT = '568px';
+export const WITHOUT_SEARCH_BOX_HEIGHT = '513px';
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/images/integrations_icon.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/images/integrations_icon.png
new file mode 100644
index 0000000000000..438e220084c46
Binary files /dev/null and b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/images/integrations_icon.png differ
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/index.ts
new file mode 100644
index 0000000000000..10812c565801b
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/index.ts
@@ -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 React from 'react';
+import { i18n } from '@kbn/i18n';
+import type { OnboardingCardConfig } from '../../../../types';
+import { checkIntegrationsCardComplete } from './integrations_check_complete';
+import { OnboardingCardId } from '../../../../constants';
+import integrationsIcon from './images/integrations_icon.png';
+import type { IntegrationCardMetadata } from './types';
+
+export const integrationsCardConfig: OnboardingCardConfig = {
+ id: OnboardingCardId.integrations,
+ title: i18n.translate('xpack.securitySolution.onboarding.integrationsCard.title', {
+ defaultMessage: 'Add data with integrations',
+ }),
+ icon: integrationsIcon,
+ Component: React.lazy(
+ () =>
+ import(
+ /* webpackChunkName: "onboarding_integrations_card" */
+ './integrations_card'
+ )
+ ),
+ checkComplete: checkIntegrationsCardComplete,
+ capabilities: ['fleet.all', 'fleetv2.all'],
+ licenseType: 'basic',
+};
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.test.tsx
new file mode 100644
index 0000000000000..f55cc8cd50b2d
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.test.tsx
@@ -0,0 +1,169 @@
+/*
+ * 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 { render, fireEvent, waitFor, act } from '@testing-library/react';
+
+import { IntegrationsCardGridTabsComponent } from './integration_card_grid_tabs';
+import * as module from '@kbn/fleet-plugin/public';
+
+import {
+ useStoredIntegrationSearchTerm,
+ useStoredIntegrationTabId,
+} from '../../../../hooks/use_stored_state';
+import { DEFAULT_TAB } from './constants';
+
+jest.mock('../../../onboarding_context');
+jest.mock('../../../../hooks/use_stored_state');
+
+jest.mock('../../../../../common/lib/kibana', () => ({
+ ...jest.requireActual('../../../../../common/lib/kibana'),
+ useNavigation: jest.fn().mockReturnValue({
+ navigateTo: jest.fn(),
+ getAppUrl: jest.fn(),
+ }),
+}));
+
+const mockPackageList = jest.fn().mockReturnValue();
+
+jest.mock('@kbn/fleet-plugin/public');
+jest
+ .spyOn(module, 'PackageList')
+ .mockImplementation(() => Promise.resolve({ PackageListGrid: mockPackageList }));
+
+describe('IntegrationsCardGridTabsComponent', () => {
+ const mockUseAvailablePackages = jest.fn();
+ const mockSetTabId = jest.fn();
+ const mockSetCategory = jest.fn();
+ const mockSetSelectedSubCategory = jest.fn();
+ const mockSetSearchTerm = jest.fn();
+ const props = {
+ installedIntegrationsCount: 1,
+ isAgentRequired: false,
+ useAvailablePackages: mockUseAvailablePackages,
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ (useStoredIntegrationTabId as jest.Mock).mockReturnValue([DEFAULT_TAB.id, jest.fn()]);
+ (useStoredIntegrationSearchTerm as jest.Mock).mockReturnValue(['', jest.fn()]);
+ });
+
+ it('renders loading skeleton when data is loading', () => {
+ mockUseAvailablePackages.mockReturnValue({
+ isLoading: true,
+ filteredCards: [],
+ setCategory: mockSetCategory,
+ setSelectedSubCategory: mockSetSelectedSubCategory,
+ setSearchTerm: mockSetSearchTerm,
+ });
+
+ const { getByTestId } = render(
+
+ );
+
+ expect(getByTestId('loadingPackages')).toBeInTheDocument();
+ });
+
+ it('renders the package list when data is available', async () => {
+ mockUseAvailablePackages.mockReturnValue({
+ isLoading: false,
+ filteredCards: [{ id: 'card1', name: 'Card 1', url: 'https://mock-url' }],
+ setCategory: mockSetCategory,
+ setSelectedSubCategory: mockSetSelectedSubCategory,
+ setSearchTerm: mockSetSearchTerm,
+ });
+
+ const { getByTestId } = render(
+
+ );
+
+ await waitFor(() => {
+ expect(getByTestId('packageList')).toBeInTheDocument();
+ });
+ });
+
+ it('saves the selected tab to storage', () => {
+ (useStoredIntegrationTabId as jest.Mock).mockReturnValue(['recommended', mockSetTabId]);
+
+ mockUseAvailablePackages.mockReturnValue({
+ isLoading: false,
+ filteredCards: [],
+ setCategory: mockSetCategory,
+ setSelectedSubCategory: mockSetSelectedSubCategory,
+ setSearchTerm: mockSetSearchTerm,
+ });
+
+ const { getByTestId } = render(
+
+ );
+
+ const tabButton = getByTestId('user');
+
+ act(() => {
+ fireEvent.click(tabButton);
+ });
+ expect(mockSetTabId).toHaveBeenCalledWith('user');
+ });
+
+ it('renders no search tools when showSearchTools is false', async () => {
+ mockUseAvailablePackages.mockReturnValue({
+ isLoading: false,
+ filteredCards: [],
+ setCategory: mockSetCategory,
+ setSelectedSubCategory: mockSetSelectedSubCategory,
+ setSearchTerm: mockSetSearchTerm,
+ });
+
+ render(
+
+ );
+
+ await waitFor(() => {
+ expect(mockPackageList.mock.calls[0][0].showSearchTools).toEqual(false);
+ });
+ });
+
+ it('updates the search term when the search input changes', async () => {
+ const mockSetSearchTermToStorage = jest.fn();
+ (useStoredIntegrationSearchTerm as jest.Mock).mockReturnValue([
+ 'new search term',
+ mockSetSearchTermToStorage,
+ ]);
+
+ mockUseAvailablePackages.mockReturnValue({
+ isLoading: false,
+ filteredCards: [],
+ setCategory: mockSetCategory,
+ setSelectedSubCategory: mockSetSelectedSubCategory,
+ setSearchTerm: mockSetSearchTerm,
+ searchTerm: 'new search term',
+ });
+
+ render(
+
+ );
+
+ await waitFor(() => {
+ expect(mockPackageList.mock.calls[0][0].searchTerm).toEqual('new search term');
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.tsx
new file mode 100644
index 0000000000000..fc30fb0d6c617
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_card_grid_tabs.tsx
@@ -0,0 +1,207 @@
+/*
+ * 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, { lazy, Suspense, useMemo, useCallback, useEffect, useRef } from 'react';
+import { EuiButtonGroup, EuiFlexGroup, EuiFlexItem, EuiSkeletonText } from '@elastic/eui';
+import type { AvailablePackagesHookType, IntegrationCardItem } from '@kbn/fleet-plugin/public';
+import { noop } from 'lodash';
+
+import { css } from '@emotion/react';
+import { withLazyHook } from '../../../../../common/components/with_lazy_hook';
+import {
+ useStoredIntegrationSearchTerm,
+ useStoredIntegrationTabId,
+} from '../../../../hooks/use_stored_state';
+import { useOnboardingContext } from '../../../onboarding_context';
+import {
+ DEFAULT_TAB,
+ LOADING_SKELETON_TEXT_LINES,
+ SCROLL_ELEMENT_ID,
+ SEARCH_FILTER_CATEGORIES,
+ WITHOUT_SEARCH_BOX_HEIGHT,
+ WITH_SEARCH_BOX_HEIGHT,
+} from './constants';
+import { INTEGRATION_TABS, INTEGRATION_TABS_BY_ID } from './integration_tabs_configs';
+import { useIntegrationCardList } from './use_integration_card_list';
+import { IntegrationTabId } from './types';
+import { IntegrationCardTopCallout } from './callouts/integration_card_top_callout';
+
+export interface IntegrationsCardGridTabsProps {
+ installedIntegrationsCount: number;
+ isAgentRequired: boolean;
+ useAvailablePackages: AvailablePackagesHookType;
+}
+
+const emptyStateStyles = { paddingTop: '16px' };
+
+export const PackageListGrid = lazy(async () => ({
+ default: await import('@kbn/fleet-plugin/public')
+ .then((module) => module.PackageList())
+ .then((pkg) => pkg.PackageListGrid),
+}));
+
+export const IntegrationsCardGridTabsComponent = React.memo(
+ ({ installedIntegrationsCount, isAgentRequired, useAvailablePackages }) => {
+ const { spaceId } = useOnboardingContext();
+ const scrollElement = useRef(null);
+ const [toggleIdSelected, setSelectedTabIdToStorage] = useStoredIntegrationTabId(
+ spaceId,
+ DEFAULT_TAB.id
+ );
+ const [searchTermFromStorage, setSearchTermToStorage] = useStoredIntegrationSearchTerm(spaceId);
+ const onTabChange = useCallback(
+ (stringId: string) => {
+ const id = stringId as IntegrationTabId;
+ scrollElement.current?.scrollTo?.(0, 0);
+ setSelectedTabIdToStorage(id);
+ },
+ [setSelectedTabIdToStorage]
+ );
+
+ const {
+ filteredCards,
+ isLoading,
+ searchTerm,
+ setCategory,
+ setSearchTerm,
+ setSelectedSubCategory,
+ } = useAvailablePackages({
+ prereleaseIntegrationsEnabled: false,
+ });
+
+ const selectedTab = useMemo(() => INTEGRATION_TABS_BY_ID[toggleIdSelected], [toggleIdSelected]);
+
+ const onSearchTermChanged = useCallback(
+ (searchQuery: string) => {
+ setSearchTerm(searchQuery);
+ // Search term is preserved across VISIBLE tabs
+ // As we want user to be able to see the same search results when coming back from Fleet
+ if (selectedTab.showSearchTools) {
+ setSearchTermToStorage(searchQuery);
+ }
+ },
+ [selectedTab.showSearchTools, setSearchTerm, setSearchTermToStorage]
+ );
+
+ useEffect(() => {
+ setCategory(selectedTab.category ?? '');
+ setSelectedSubCategory(selectedTab.subCategory);
+ if (!selectedTab.showSearchTools) {
+ // If search box are not shown, clear the search term to avoid unexpected filtering
+ onSearchTermChanged('');
+ }
+
+ if (
+ selectedTab.showSearchTools &&
+ searchTermFromStorage &&
+ toggleIdSelected !== IntegrationTabId.recommended
+ ) {
+ setSearchTerm(searchTermFromStorage);
+ }
+ }, [
+ onSearchTermChanged,
+ searchTermFromStorage,
+ selectedTab.category,
+ selectedTab.showSearchTools,
+ selectedTab.subCategory,
+ setCategory,
+ setSearchTerm,
+ setSelectedSubCategory,
+ toggleIdSelected,
+ ]);
+
+ const list: IntegrationCardItem[] = useIntegrationCardList({
+ integrationsList: filteredCards,
+ featuredCardIds: selectedTab.featuredCardIds,
+ });
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+ return (
+
+
+
+
+
+ }
+ >
+
+ }
+ calloutTopSpacerSize="m"
+ categories={SEARCH_FILTER_CATEGORIES} // We do not want to show categories and subcategories as the search bar filter
+ emptyStateStyles={emptyStateStyles}
+ list={list}
+ scrollElementId={SCROLL_ELEMENT_ID}
+ searchTerm={searchTerm}
+ selectedCategory={selectedTab.category ?? ''}
+ selectedSubCategory={selectedTab.subCategory}
+ setCategory={setCategory}
+ setSearchTerm={onSearchTermChanged}
+ setUrlandPushHistory={noop}
+ setUrlandReplaceHistory={noop}
+ showCardLabels={false}
+ showControls={false}
+ showSearchTools={selectedTab.showSearchTools}
+ sortByFeaturedIntegrations={selectedTab.sortByFeaturedIntegrations}
+ spacer={false}
+ />
+
+
+
+ );
+ }
+);
+IntegrationsCardGridTabsComponent.displayName = 'IntegrationsCardGridTabsComponent';
+
+export const IntegrationsCardGridTabs = withLazyHook(
+ IntegrationsCardGridTabsComponent,
+ () => import('@kbn/fleet-plugin/public').then((module) => module.AvailablePackagesHook()),
+
+);
+IntegrationsCardGridTabs.displayName = 'IntegrationsCardGridTabs';
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_tabs_configs.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_tabs_configs.ts
new file mode 100644
index 0000000000000..2e673d98278a3
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integration_tabs_configs.ts
@@ -0,0 +1,83 @@
+/*
+ * 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 { IntegrationTabId, type Tab } from './types';
+
+export const INTEGRATION_TABS: Tab[] = [
+ {
+ category: '',
+ iconType: 'starFilled',
+ id: IntegrationTabId.recommended,
+ label: 'Recommended',
+ overflow: 'hidden',
+ showSearchTools: false,
+ // Fleet has a default sorting for integrations by category that Security Solution does not want to apply
+ // so we need to disable the sorting for the recommended tab to allow static ordering according to the featuredCardIds
+ sortByFeaturedIntegrations: false,
+ featuredCardIds: [
+ 'epr:aws',
+ 'epr:gcp',
+ 'epr:azure',
+ 'epr:endpoint',
+ 'epr:crowdstrike',
+ 'epr:wiz',
+ 'epr:network_traffic',
+ 'epr:osquery_manager',
+ ],
+ },
+ {
+ category: 'security',
+ id: IntegrationTabId.network,
+ label: 'Network',
+ subCategory: 'network',
+ showSearchTools: true,
+ sortByFeaturedIntegrations: true,
+ },
+ {
+ category: 'security',
+ id: IntegrationTabId.user,
+ label: 'User',
+ subCategory: 'iam',
+ showSearchTools: true,
+ sortByFeaturedIntegrations: true,
+ },
+ {
+ category: 'security',
+ id: IntegrationTabId.endpoint,
+ label: 'Endpoint',
+ subCategory: 'edr_xdr',
+ showSearchTools: true,
+ sortByFeaturedIntegrations: true,
+ },
+ {
+ category: 'security',
+ id: IntegrationTabId.cloud,
+ label: 'Cloud',
+ subCategory: 'cloudsecurity_cdr',
+ showSearchTools: true,
+ sortByFeaturedIntegrations: true,
+ },
+ {
+ category: 'security',
+ id: IntegrationTabId.threatIntel,
+ label: 'Threat Intel',
+ subCategory: 'threat_intel',
+ showSearchTools: true,
+ sortByFeaturedIntegrations: true,
+ },
+ {
+ category: '',
+ id: IntegrationTabId.all,
+ label: 'All',
+ showSearchTools: true,
+ sortByFeaturedIntegrations: true,
+ },
+];
+
+export const INTEGRATION_TABS_BY_ID = Object.fromEntries(
+ INTEGRATION_TABS.map((tab) => [tab.id, tab])
+) as Record;
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_card.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_card.test.tsx
new file mode 100644
index 0000000000000..3f79745182c5a
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_card.test.tsx
@@ -0,0 +1,31 @@
+/*
+ * 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 IntegrationsCard from './integrations_card';
+import { render } from '@testing-library/react';
+jest.mock('./integration_card_grid_tabs');
+
+const props = {
+ setComplete: jest.fn(),
+ checkComplete: jest.fn(),
+ isCardComplete: jest.fn(),
+ setExpandedCardId: jest.fn(),
+};
+
+describe('IntegrationsCard', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders a loading spinner when checkCompleteMetadata is undefined', () => {
+ const { getByTestId } = render(
+
+ );
+ expect(getByTestId('loadingInstalledIntegrations')).toBeInTheDocument();
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_card.tsx
new file mode 100644
index 0000000000000..2fafc0405efe4
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_card.tsx
@@ -0,0 +1,35 @@
+/*
+ * 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 { OnboardingCardComponent } from '../../../../types';
+import { OnboardingCardContentPanel } from '../common/card_content_panel';
+import { IntegrationsCardGridTabs } from './integration_card_grid_tabs';
+import { CenteredLoadingSpinner } from '../../../../../common/components/centered_loading_spinner';
+import type { IntegrationCardMetadata } from './types';
+
+export const IntegrationsCard: OnboardingCardComponent = React.memo(
+ ({ checkCompleteMetadata }) => {
+ if (!checkCompleteMetadata) {
+ return ;
+ }
+ const { installedIntegrationsCount, isAgentRequired } = checkCompleteMetadata;
+
+ return (
+
+
+
+ );
+ }
+);
+IntegrationsCard.displayName = 'IntegrationsCard';
+
+// eslint-disable-next-line import/no-default-export
+export default IntegrationsCard;
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_check_complete.test.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_check_complete.test.ts
new file mode 100644
index 0000000000000..3dd19d8868390
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_check_complete.test.ts
@@ -0,0 +1,106 @@
+/*
+ * 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 { checkIntegrationsCardComplete } from './integrations_check_complete';
+import { installationStatuses } from '@kbn/fleet-plugin/public';
+import type { StartServices } from '../../../../../types';
+
+import { lastValueFrom } from 'rxjs';
+
+jest.mock('rxjs', () => ({
+ ...jest.requireActual('rxjs'),
+ lastValueFrom: jest.fn(),
+}));
+
+describe('checkIntegrationsCardComplete', () => {
+ const mockHttpGet: jest.Mock = jest.fn();
+ const mockSearch: jest.Mock = jest.fn();
+ const mockService = {
+ http: {
+ get: mockHttpGet,
+ },
+ data: {
+ search: {
+ search: mockSearch,
+ },
+ },
+ } as unknown as StartServices;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('returns isComplete as false when no packages are installed', async () => {
+ mockHttpGet.mockResolvedValue({
+ items: [],
+ });
+
+ (lastValueFrom as jest.Mock).mockResolvedValue({
+ rawResponse: {
+ hits: { total: 0 },
+ },
+ });
+
+ const result = await checkIntegrationsCardComplete(mockService);
+
+ expect(result).toEqual({
+ isComplete: false,
+ metadata: {
+ installedIntegrationsCount: 0,
+ isAgentRequired: false,
+ },
+ });
+ });
+
+ it('returns isComplete as true when packages are installed but no agent data is available', async () => {
+ mockHttpGet.mockResolvedValue({
+ items: [{ status: installationStatuses.Installed }],
+ });
+
+ (lastValueFrom as jest.Mock).mockResolvedValue({
+ rawResponse: {
+ hits: { total: 0 },
+ },
+ });
+
+ const result = await checkIntegrationsCardComplete(mockService);
+
+ expect(result).toEqual({
+ isComplete: true,
+ completeBadgeText: '1 integration added',
+ metadata: {
+ installedIntegrationsCount: 1,
+ isAgentRequired: true,
+ },
+ });
+ });
+
+ it('returns isComplete as true and isAgentRequired as false when both packages and agent data are available', async () => {
+ mockHttpGet.mockResolvedValue({
+ items: [
+ { status: installationStatuses.Installed },
+ { status: installationStatuses.InstallFailed },
+ ],
+ });
+
+ (lastValueFrom as jest.Mock).mockResolvedValue({
+ rawResponse: {
+ hits: { total: 1 },
+ },
+ });
+
+ const result = await checkIntegrationsCardComplete(mockService);
+
+ expect(result).toEqual({
+ isComplete: true,
+ completeBadgeText: '2 integrations added',
+ metadata: {
+ installedIntegrationsCount: 2,
+ isAgentRequired: false,
+ },
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_check_complete.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_check_complete.ts
new file mode 100644
index 0000000000000..912b81bddf3fb
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/integrations_check_complete.ts
@@ -0,0 +1,68 @@
+/*
+ * 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 { GetPackagesResponse } from '@kbn/fleet-plugin/public';
+import { EPM_API_ROUTES, installationStatuses } from '@kbn/fleet-plugin/public';
+import { i18n } from '@kbn/i18n';
+import { lastValueFrom } from 'rxjs';
+import type { OnboardingCardCheckComplete } from '../../../../types';
+import { AGENT_INDEX } from './constants';
+import type { StartServices } from '../../../../../types';
+import type { IntegrationCardMetadata } from './types';
+
+export const checkIntegrationsCardComplete: OnboardingCardCheckComplete<
+ IntegrationCardMetadata
+> = async (services: StartServices) => {
+ const packageData = await services.http.get(
+ EPM_API_ROUTES.INSTALL_BY_UPLOAD_PATTERN,
+ {
+ version: '2023-10-31',
+ }
+ );
+
+ const agentsData = await lastValueFrom(
+ services.data.search.search({
+ params: { index: AGENT_INDEX, body: { size: 1 } },
+ })
+ );
+
+ const installed = packageData?.items?.filter(
+ (pkg) =>
+ pkg.status === installationStatuses.Installed ||
+ pkg.status === installationStatuses.InstallFailed
+ );
+ const isComplete = installed && installed.length > 0;
+ const agentsDataAvailable = !!agentsData?.rawResponse?.hits?.total;
+ const isAgentRequired = isComplete && !agentsDataAvailable;
+
+ const completeBadgeText = i18n.translate(
+ 'xpack.securitySolution.onboarding.integrationsCard.badge.completeText',
+ {
+ defaultMessage: '{count} {count, plural, one {integration} other {integrations}} added',
+ values: { count: installed.length },
+ }
+ );
+
+ if (!isComplete) {
+ return {
+ isComplete,
+ metadata: {
+ installedIntegrationsCount: 0,
+ isAgentRequired: false,
+ },
+ };
+ }
+
+ return {
+ isComplete,
+ completeBadgeText,
+ metadata: {
+ installedIntegrationsCount: installed.length,
+ isAgentRequired,
+ },
+ };
+};
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/types.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/types.ts
new file mode 100644
index 0000000000000..849e9cdd2336b
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/types.ts
@@ -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.
+ */
+export interface Tab {
+ category: string;
+ featuredCardIds?: string[];
+ iconType?: string;
+ id: IntegrationTabId;
+ label: string;
+ overflow?: 'hidden' | 'scroll';
+ showSearchTools?: boolean;
+ subCategory?: string;
+ sortByFeaturedIntegrations: boolean;
+}
+
+export enum IntegrationTabId {
+ recommended = 'recommended',
+ network = 'network',
+ user = 'user',
+ endpoint = 'endpoint',
+ cloud = 'cloud',
+ threatIntel = 'threatIntel',
+ all = 'all',
+}
+
+export interface IntegrationCardMetadata {
+ installedIntegrationsCount: number;
+ isAgentRequired: boolean;
+}
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.test.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.test.ts
new file mode 100644
index 0000000000000..9c4e1978f27b7
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.test.ts
@@ -0,0 +1,76 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { renderHook } from '@testing-library/react-hooks';
+import { useIntegrationCardList } from './use_integration_card_list';
+
+jest.mock('../../../../../common/lib/kibana', () => ({
+ ...jest.requireActual('../../../../../common/lib/kibana'),
+ useNavigation: jest.fn().mockReturnValue({
+ navigateTo: jest.fn(),
+ getAppUrl: jest.fn(),
+ }),
+}));
+
+describe('useIntegrationCardList', () => {
+ const mockIntegrationsList = [
+ {
+ id: 'epr:endpoint',
+ name: 'Security Integration',
+ description: 'Integration for security monitoring',
+ categories: ['security'],
+ icons: [{ src: 'icon_url', type: 'image' }],
+ integration: 'security',
+ maxCardHeight: 127,
+ onCardClick: expect.any(Function),
+ showInstallStatus: true,
+ titleLineClamp: 1,
+ descriptionLineClamp: 3,
+ showInstallationStatus: true,
+ title: 'Security Integration',
+ url: '/app/integrations/security',
+ version: '1.0.0',
+ },
+ ];
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('returns filtered integration cards when featuredCardIds are not provided', () => {
+ const mockFilteredCards = {
+ featuredCards: {},
+ integrationCards: mockIntegrationsList,
+ };
+
+ const { result } = renderHook(() =>
+ useIntegrationCardList({
+ integrationsList: mockIntegrationsList,
+ })
+ );
+
+ expect(result.current).toEqual(mockFilteredCards.integrationCards);
+ });
+
+ it('returns featured cards when featuredCardIds are provided', () => {
+ const featuredCardIds = ['epr:endpoint'];
+ const mockFilteredCards = {
+ featuredCards: {
+ 'epr:endpoint': mockIntegrationsList[0],
+ },
+ integrationCards: mockIntegrationsList,
+ };
+
+ const { result } = renderHook(() =>
+ useIntegrationCardList({
+ integrationsList: mockIntegrationsList,
+ featuredCardIds,
+ })
+ );
+
+ expect(result.current).toEqual([mockFilteredCards.featuredCards['epr:endpoint']]);
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.ts
new file mode 100644
index 0000000000000..2a9675f91e9a8
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/integrations/use_integration_card_list.ts
@@ -0,0 +1,133 @@
+/*
+ * 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 { useMemo } from 'react';
+import type { IntegrationCardItem } from '@kbn/fleet-plugin/public';
+import { SECURITY_UI_APP_ID } from '@kbn/security-solution-navigation';
+import { useNavigation } from '../../../../../common/lib/kibana';
+import {
+ APP_INTEGRATIONS_PATH,
+ APP_UI_ID,
+ ONBOARDING_PATH,
+} from '../../../../../../common/constants';
+import {
+ CARD_DESCRIPTION_LINE_CLAMP,
+ CARD_TITLE_LINE_CLAMP,
+ INTEGRATION_APP_ID,
+ MAX_CARD_HEIGHT_IN_PX,
+ ONBOARDING_APP_ID,
+ ONBOARDING_LINK,
+} from './constants';
+import type { GetAppUrl, NavigateTo } from '../../../../../common/lib/kibana';
+
+const addPathParamToUrl = (url: string, onboardingLink: string) => {
+ const encoded = encodeURIComponent(onboardingLink);
+ const paramsString = `${ONBOARDING_LINK}=${encoded}&${ONBOARDING_APP_ID}=${APP_UI_ID}`;
+
+ if (url.indexOf('?') >= 0) {
+ return `${url}&${paramsString}`;
+ }
+ return `${url}?${paramsString}`;
+};
+
+const extractFeaturedCards = (filteredCards: IntegrationCardItem[], featuredCardIds: string[]) => {
+ return filteredCards.reduce((acc, card) => {
+ if (featuredCardIds.includes(card.id)) {
+ acc.push(card);
+ }
+ return acc;
+ }, []);
+};
+
+const getFilteredCards = ({
+ featuredCardIds,
+ getAppUrl,
+ installedIntegrationList,
+ integrationsList,
+ navigateTo,
+}: {
+ featuredCardIds?: string[];
+ getAppUrl: GetAppUrl;
+ installedIntegrationList?: IntegrationCardItem[];
+ integrationsList: IntegrationCardItem[];
+ navigateTo: NavigateTo;
+}) => {
+ const securityIntegrationsList = integrationsList.map((card) =>
+ addSecuritySpecificProps({ navigateTo, getAppUrl, card, installedIntegrationList })
+ );
+ if (!featuredCardIds) {
+ return { featuredCards: [], integrationCards: securityIntegrationsList };
+ }
+ const featuredCards = extractFeaturedCards(securityIntegrationsList, featuredCardIds);
+ return {
+ featuredCards,
+ integrationCards: securityIntegrationsList,
+ };
+};
+
+const addSecuritySpecificProps = ({
+ navigateTo,
+ getAppUrl,
+ card,
+}: {
+ navigateTo: NavigateTo;
+ getAppUrl: GetAppUrl;
+ card: IntegrationCardItem;
+ installedIntegrationList?: IntegrationCardItem[];
+}): IntegrationCardItem => {
+ const onboardingLink = getAppUrl({ appId: SECURITY_UI_APP_ID, path: ONBOARDING_PATH });
+ const integrationRootUrl = getAppUrl({ appId: INTEGRATION_APP_ID });
+ const state = {
+ onCancelNavigateTo: [APP_UI_ID, { path: ONBOARDING_PATH }],
+ onCancelUrl: onboardingLink,
+ onSaveNavigateTo: [APP_UI_ID, { path: ONBOARDING_PATH }],
+ };
+ const url =
+ card.url.indexOf(APP_INTEGRATIONS_PATH) >= 0 && onboardingLink
+ ? addPathParamToUrl(card.url, onboardingLink)
+ : card.url;
+ return {
+ ...card,
+ titleLineClamp: CARD_TITLE_LINE_CLAMP,
+ descriptionLineClamp: CARD_DESCRIPTION_LINE_CLAMP,
+ maxCardHeight: MAX_CARD_HEIGHT_IN_PX,
+ showInstallationStatus: true,
+ url,
+ onCardClick: () => {
+ if (url.startsWith(APP_INTEGRATIONS_PATH)) {
+ navigateTo({
+ appId: INTEGRATION_APP_ID,
+ path: url.slice(integrationRootUrl.length),
+ state,
+ });
+ } else if (url.startsWith('http') || url.startsWith('https')) {
+ window.open(url, '_blank');
+ } else {
+ navigateTo({ url, state });
+ }
+ },
+ };
+};
+
+export const useIntegrationCardList = ({
+ integrationsList,
+ featuredCardIds,
+}: {
+ integrationsList: IntegrationCardItem[];
+ featuredCardIds?: string[] | undefined;
+}): IntegrationCardItem[] => {
+ const { navigateTo, getAppUrl } = useNavigation();
+
+ const { featuredCards, integrationCards } = useMemo(
+ () => getFilteredCards({ navigateTo, getAppUrl, integrationsList, featuredCardIds }),
+ [navigateTo, getAppUrl, integrationsList, featuredCardIds]
+ );
+
+ if (featuredCardIds && featuredCardIds.length > 0) {
+ return featuredCards;
+ }
+ return integrationCards ?? [];
+};
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/images/rules.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/images/rules.png
new file mode 100644
index 0000000000000..5d88e8c95d43c
Binary files /dev/null and b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/images/rules.png differ
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/images/rules_icon.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/images/rules_icon.png
new file mode 100644
index 0000000000000..b2b4848e0be1d
Binary files /dev/null and b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/images/rules_icon.png differ
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/index.ts
new file mode 100644
index 0000000000000..6bd8e0cbcb7a1
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/index.ts
@@ -0,0 +1,27 @@
+/*
+ * 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 { OnboardingCardConfig } from '../../../../types';
+import { OnboardingCardId } from '../../../../constants';
+import { RULES_CARD_TITLE } from './translations';
+import rulesIcon from './images/rules_icon.png';
+import { checkRulesComplete } from './rules_check_complete';
+
+export const rulesCardConfig: OnboardingCardConfig = {
+ id: OnboardingCardId.rules,
+ title: RULES_CARD_TITLE,
+ icon: rulesIcon,
+ Component: React.lazy(
+ () =>
+ import(
+ /* webpackChunkName: "onboarding_rules_card" */
+ './rules_card'
+ )
+ ),
+ checkComplete: checkRulesComplete,
+};
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/rules_card.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/rules_card.test.tsx
new file mode 100644
index 0000000000000..f7156adc34eba
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/rules_card.test.tsx
@@ -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 React from 'react';
+import { render } from '@testing-library/react';
+import { RulesCard } from './rules_card';
+import { TestProviders } from '../../../../../common/mock';
+
+const props = {
+ setComplete: jest.fn(),
+ checkComplete: jest.fn(),
+ isCardComplete: jest.fn(),
+ setExpandedCardId: jest.fn(),
+};
+
+describe('RulesCard', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('description should be in the document', () => {
+ const { getByTestId } = render(
+
+
+
+ );
+
+ expect(getByTestId('rulesCardDescription')).toBeInTheDocument();
+ });
+
+ it('card callout should be rendered if integrations cards is not complete', () => {
+ props.isCardComplete.mockReturnValueOnce(false);
+
+ const { getByText } = render(
+
+
+
+ );
+
+ expect(getByText('To add Elastic rules add integrations first.')).toBeInTheDocument();
+ });
+
+ it('card button should be disabled if integrations cards is not complete', () => {
+ props.isCardComplete.mockReturnValueOnce(false);
+
+ const { getByTestId } = render(
+
+
+
+ );
+
+ expect(getByTestId('rulesCardButton').querySelector('button')).toBeDisabled();
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/rules_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/rules_card.tsx
new file mode 100644
index 0000000000000..7f283c0ffbc78
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/rules_card.tsx
@@ -0,0 +1,77 @@
+/*
+ * 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, { useCallback, useMemo } from 'react';
+import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiSpacer, EuiText } from '@elastic/eui';
+import { SecurityPageName } from '@kbn/security-solution-navigation';
+import { SecuritySolutionLinkButton } from '../../../../../common/components/links';
+import { OnboardingCardId } from '../../../../constants';
+import type { OnboardingCardComponent } from '../../../../types';
+import { OnboardingCardContentImagePanel } from '../common/card_content_image_panel';
+import { CardCallOut } from '../common/card_callout';
+import rulesImageSrc from './images/rules.png';
+import * as i18n from './translations';
+
+export const RulesCard: OnboardingCardComponent = ({ isCardComplete, setExpandedCardId }) => {
+ const isIntegrationsCardComplete = useMemo(
+ () => isCardComplete(OnboardingCardId.integrations),
+ [isCardComplete]
+ );
+
+ const expandIntegrationsCard = useCallback(() => {
+ setExpandedCardId(OnboardingCardId.integrations, { scroll: true });
+ }, [setExpandedCardId]);
+
+ return (
+
+
+
+
+ {i18n.RULES_CARD_DESCRIPTION}
+
+ {!isIntegrationsCardComplete && (
+ <>
+
+
+
+ {i18n.RULES_CARD_CALLOUT_INTEGRATIONS_BUTTON}
+
+
+
+
+
+ }
+ />
+ >
+ )}
+
+
+
+ {i18n.RULES_CARD_ADD_RULES_BUTTON}
+
+
+
+
+ );
+};
+
+// eslint-disable-next-line import/no-default-export
+export default RulesCard;
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/apis/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/rules_check_complete.ts
similarity index 50%
rename from x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/apis/index.ts
rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/rules_check_complete.ts
index 7149d1ecf6c72..3679141a255b4 100644
--- a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/apis/index.ts
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/rules_check_complete.ts
@@ -6,10 +6,40 @@
*/
import type { HttpSetup } from '@kbn/core/public';
+import type { OnboardingCardCheckComplete } from '../../../../types';
+import { ENABLED_FIELD } from '../../../../../../common';
import { DETECTION_ENGINE_RULES_URL_FIND } from '../../../../../../common/constants';
-import type { FetchRulesResponse } from '../../../../../detection_engine/rule_management/logic/types';
+import type { FetchRulesResponse } from '../../../../../detection_engine/rule_management/logic';
-export const fetchRuleManagementFilters = async ({
+export const checkRulesComplete: OnboardingCardCheckComplete = async ({
+ http,
+ notifications: { toasts },
+}) => {
+ // Check if there are any rules installed and enabled
+ try {
+ const data = await fetchRuleManagementFilters({
+ http,
+ query: {
+ page: 1,
+ per_page: 20,
+ sort_field: 'enabled',
+ sort_order: 'desc',
+ filter: `${ENABLED_FIELD}: true`,
+ },
+ });
+ return {
+ isComplete: data?.total > 0,
+ };
+ } catch (e) {
+ toasts.addError(e, { title: `Failed to check Card Rules completion.` });
+
+ return {
+ isComplete: false,
+ };
+ }
+};
+
+const fetchRuleManagementFilters = async ({
http,
signal,
query,
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/translations.ts
new file mode 100644
index 0000000000000..81e0919cd7184
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/cards/rules/translations.ts
@@ -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 { i18n } from '@kbn/i18n';
+
+export const RULES_CARD_TITLE = i18n.translate(
+ 'xpack.securitySolution.onboarding.rulesCard.title',
+ {
+ defaultMessage: 'Enable rules',
+ }
+);
+
+export const RULES_CARD_DESCRIPTION = i18n.translate(
+ 'xpack.securitySolution.onboarding.rulesCard.description',
+ {
+ defaultMessage:
+ 'Elastic Security comes with prebuilt detection rules that run in the background and create alerts when their conditions are met.',
+ }
+);
+
+export const RULES_CARD_CALLOUT_INTEGRATIONS_TEXT = i18n.translate(
+ 'xpack.securitySolution.onboarding.rulesCard.calloutIntegrationsText',
+ {
+ defaultMessage: 'To add Elastic rules add integrations first.',
+ }
+);
+
+export const RULES_CARD_CALLOUT_INTEGRATIONS_BUTTON = i18n.translate(
+ 'xpack.securitySolution.onboarding.rulesCard.calloutIntegrationsButton',
+ {
+ defaultMessage: 'Add integrations step',
+ }
+);
+
+export const RULES_CARD_ADD_RULES_BUTTON = i18n.translate(
+ 'xpack.securitySolution.onboarding.rulesCard.addRulesButton',
+ {
+ defaultMessage: 'Add Elastic rules',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_body_config.test.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_body_config.test.ts
new file mode 100644
index 0000000000000..19e80e4005a59
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_body_config.test.ts
@@ -0,0 +1,117 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { renderHook } from '@testing-library/react-hooks';
+import { useBodyConfig } from './use_body_config';
+import { useKibana } from '../../../../common/lib/kibana/kibana_react';
+import { useObservable } from 'react-use';
+import { hasCapabilities } from '../../../../common/lib/capabilities';
+
+const bodyConfig = [
+ {
+ title: 'Group 1',
+ cards: [
+ {
+ id: 'license_card',
+ title: 'licensed card',
+ icon: 'fleetApp',
+ licenseType: 'platinum',
+ },
+ {
+ id: 'capabilities_card',
+ title: 'rbac card',
+ icon: 'fleetApp',
+ capabilities: ['siem.crud'],
+ },
+ ],
+ },
+ {
+ title: 'Group 2',
+ cards: [
+ {
+ id: 'capabilities_license_card',
+ title: 'all card',
+ icon: 'fleetApp',
+ capabilities: ['siem.crud'],
+ licenseType: 'platinum',
+ },
+ ],
+ },
+];
+
+// Mock dependencies
+jest.mock('react-use');
+jest.mock('../../../../common/lib/kibana/kibana_react');
+jest.mock('../../../../common/lib/capabilities');
+jest.mock('../body_config', () => ({ bodyConfig }));
+
+const mockLicenseHasAtLeast = jest.fn();
+const mockUseObservable = useObservable as jest.Mock;
+const mockHasCapabilities = hasCapabilities as jest.Mock;
+mockUseObservable.mockReturnValue({ hasAtLeast: mockLicenseHasAtLeast });
+
+(useKibana as jest.Mock).mockReturnValue({
+ services: { application: { capabilities: {} }, licensing: {} },
+});
+
+describe('useBodyConfig', () => {
+ beforeEach(() => {
+ mockLicenseHasAtLeast.mockReturnValue(true);
+ mockHasCapabilities.mockReturnValue(true);
+ jest.clearAllMocks();
+ });
+
+ it('should return an empty array if license is not defined', () => {
+ mockUseObservable.mockReturnValueOnce(undefined);
+ const { result } = renderHook(useBodyConfig);
+ expect(result.current).toEqual([]);
+ });
+
+ it('should return all cards if no capabilities or licenseType are filtered', () => {
+ const { result } = renderHook(useBodyConfig);
+ expect(result.current).toEqual(bodyConfig);
+ });
+
+ it('should filter out cards based on license', () => {
+ mockLicenseHasAtLeast.mockReturnValue(false);
+
+ const { result } = renderHook(useBodyConfig);
+
+ expect(result.current).toEqual([
+ {
+ title: 'Group 1',
+ cards: [
+ {
+ id: 'capabilities_card',
+ title: 'rbac card',
+ icon: 'fleetApp',
+ capabilities: ['siem.crud'],
+ },
+ ],
+ },
+ ]);
+ });
+
+ it('should filter out cards based on capabilities', () => {
+ mockHasCapabilities.mockReturnValue(false);
+
+ const { result } = renderHook(useBodyConfig);
+
+ expect(result.current).toEqual([
+ {
+ title: 'Group 1',
+ cards: [
+ {
+ id: 'license_card',
+ title: 'licensed card',
+ icon: 'fleetApp',
+ licenseType: 'platinum',
+ },
+ ],
+ },
+ ]);
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_body_config.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_body_config.ts
new file mode 100644
index 0000000000000..e140f953fb028
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_body_config.ts
@@ -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 { useObservable } from 'react-use';
+import { useMemo } from 'react';
+import { hasCapabilities } from '../../../../common/lib/capabilities';
+import { useKibana } from '../../../../common/lib/kibana/kibana_react';
+import { bodyConfig } from '../body_config';
+import type { OnboardingGroupConfig } from '../../../types';
+
+/**
+ * Hook that filters the config based on the user's capabilities and license
+ */
+export const useBodyConfig = () => {
+ const { application, licensing } = useKibana().services;
+ const license = useObservable(licensing.license$);
+
+ const filteredBodyConfig = useMemo(() => {
+ // Return empty array when the license is not defined. It should always become defined at some point.
+ // This exit case prevents code dependant on the cards config (like completion checks) from running multiple times.
+ if (!license) {
+ return [];
+ }
+ return bodyConfig.reduce((filteredGroups, group) => {
+ const filteredCards = group.cards.filter((card) => {
+ if (card.capabilities) {
+ const cardHasCapabilities = hasCapabilities(application.capabilities, card.capabilities);
+ if (!cardHasCapabilities) {
+ return false;
+ }
+ }
+
+ if (card.licenseType) {
+ const cardHasLicense = license.hasAtLeast(card.licenseType);
+ if (!cardHasLicense) {
+ return false;
+ }
+ }
+
+ return true;
+ });
+
+ if (filteredCards.length > 0) {
+ filteredGroups.push({ ...group, cards: filteredCards });
+ }
+ return filteredGroups;
+ }, []);
+ }, [license, application.capabilities]);
+
+ return filteredBodyConfig;
+};
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_completed_cards.test.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_completed_cards.test.ts
new file mode 100644
index 0000000000000..31c440e8f1415
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_completed_cards.test.ts
@@ -0,0 +1,217 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { renderHook, act, type RenderHookResult } from '@testing-library/react-hooks';
+import { waitFor } from '@testing-library/react';
+import { useCompletedCards } from './use_completed_cards';
+import type { OnboardingGroupConfig } from '../../../types';
+import type { OnboardingCardId } from '../../../constants';
+import { mockReportCardComplete } from '../../__mocks__/onboarding_context_mocks';
+
+const defaultStoredCompletedCardIds: OnboardingCardId[] = [];
+const mockSetStoredCompletedCardIds = jest.fn();
+const mockUseStoredCompletedCardIds = jest.fn(() => [
+ defaultStoredCompletedCardIds,
+ mockSetStoredCompletedCardIds,
+]);
+jest.mock('../../../hooks/use_stored_state', () => ({
+ ...jest.requireActual('../../../hooks/use_stored_state'),
+ useStoredCompletedCardIds: () => mockUseStoredCompletedCardIds(),
+}));
+
+jest.mock('../../onboarding_context');
+
+const cardComplete = {
+ id: 'card-completed' as OnboardingCardId,
+ title: 'card completed',
+ checkComplete: jest.fn().mockResolvedValue(true),
+};
+const cardComplete2 = {
+ id: 'card-completed-2' as OnboardingCardId,
+ title: 'card completed 2',
+ checkComplete: jest.fn().mockResolvedValue({ isComplete: true }),
+};
+const cardIncomplete = {
+ id: 'card-incomplete' as OnboardingCardId,
+ title: 'card incomplete',
+ checkComplete: jest.fn().mockResolvedValue(false),
+};
+const cardBadgeText = {
+ id: 'card-badge-text' as OnboardingCardId,
+ title: 'card badge text',
+ checkComplete: jest
+ .fn()
+ .mockResolvedValue({ isComplete: true, completeBadgeText: 'custom badge text' }),
+};
+const cardIncompleteAdditionalBadges = {
+ id: 'card-additional-badges' as OnboardingCardId,
+ title: 'card badge text',
+ checkComplete: jest.fn().mockResolvedValue({
+ isComplete: false,
+ additionalBadges: ['additional badge'],
+ }),
+};
+const cardMetadata = {
+ id: 'card-metadata' as OnboardingCardId,
+ title: 'card metadata',
+ checkComplete: jest
+ .fn()
+ .mockResolvedValue({ isComplete: true, metadata: { custom: 'metadata' } }),
+};
+
+const mockCardsGroupConfig = [
+ {
+ title: 'Group 1',
+ cards: [cardComplete, cardComplete2, cardIncomplete],
+ },
+ {
+ title: 'Group 2',
+ cards: [cardBadgeText, cardIncompleteAdditionalBadges, cardMetadata],
+ },
+] as unknown as OnboardingGroupConfig[];
+
+describe('useCompletedCards Hook', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('when checkComplete functions are resolved', () => {
+ let renderResult: RenderHookResult<
+ OnboardingGroupConfig[],
+ ReturnType
+ >;
+ beforeEach(async () => {
+ renderResult = renderHook(useCompletedCards, { initialProps: mockCardsGroupConfig });
+ await act(async () => {
+ await waitFor(() => {
+ expect(mockSetStoredCompletedCardIds).toHaveBeenCalledTimes(4); // number of completed cards
+ });
+ });
+ });
+
+ it('should set the correct completed card ids', async () => {
+ expect(mockSetStoredCompletedCardIds).toHaveBeenCalledWith([
+ cardComplete.id,
+ cardComplete2.id,
+ cardBadgeText.id,
+ cardMetadata.id,
+ ]);
+ expect(mockSetStoredCompletedCardIds).not.toHaveBeenCalledWith(
+ expect.arrayContaining([cardIncomplete.id])
+ );
+ expect(mockSetStoredCompletedCardIds).not.toHaveBeenCalledWith(
+ expect.arrayContaining([cardIncompleteAdditionalBadges.id])
+ );
+ });
+
+ it('should report completed card ids', () => {
+ expect(mockReportCardComplete).toHaveBeenCalledTimes(4);
+ expect(mockReportCardComplete).toHaveBeenCalledWith(cardComplete.id, { auto: true });
+ expect(mockReportCardComplete).toHaveBeenCalledWith(cardComplete2.id, { auto: true });
+ expect(mockReportCardComplete).toHaveBeenCalledWith(cardBadgeText.id, { auto: true });
+ expect(mockReportCardComplete).toHaveBeenCalledWith(cardMetadata.id, { auto: true });
+ });
+
+ it('should return the correct completed state', () => {
+ expect(renderResult.result.current.isCardComplete(cardComplete.id)).toEqual(true);
+ expect(renderResult.result.current.isCardComplete(cardComplete2.id)).toEqual(true);
+ expect(renderResult.result.current.isCardComplete(cardIncomplete.id)).toEqual(false);
+ expect(renderResult.result.current.isCardComplete(cardBadgeText.id)).toEqual(true);
+ expect(renderResult.result.current.isCardComplete(cardMetadata.id)).toEqual(true);
+ expect(renderResult.result.current.isCardComplete(cardIncompleteAdditionalBadges.id)).toEqual(
+ false
+ );
+ });
+
+ describe('when a card is marked as complete', () => {
+ beforeEach(async () => {
+ jest.clearAllMocks();
+ act(() => {
+ renderResult.result.current.setCardComplete(cardIncomplete.id, true);
+ });
+ });
+
+ it('should set the correct completed card ids', async () => {
+ expect(mockSetStoredCompletedCardIds).toHaveBeenCalledTimes(1);
+ expect(mockSetStoredCompletedCardIds).toHaveBeenCalledWith([
+ cardComplete.id,
+ cardComplete2.id,
+ cardBadgeText.id,
+ cardMetadata.id,
+ cardIncomplete.id,
+ ]);
+ });
+
+ it('should return the correct completed state', () => {
+ expect(renderResult.result.current.isCardComplete(cardIncomplete.id)).toEqual(true);
+ });
+
+ it('should report the completed card', async () => {
+ expect(mockReportCardComplete).toHaveBeenCalledTimes(1);
+ expect(mockReportCardComplete).toHaveBeenCalledWith(cardIncomplete.id, undefined);
+ });
+ });
+
+ describe('when a card is marked as incomplete', () => {
+ beforeEach(async () => {
+ jest.clearAllMocks();
+ act(() => {
+ renderResult.result.current.setCardComplete(cardComplete.id, false);
+ });
+ });
+
+ it('should set the correct completed card ids', async () => {
+ expect(mockSetStoredCompletedCardIds).toHaveBeenCalledTimes(1);
+ expect(mockSetStoredCompletedCardIds).toHaveBeenCalledWith([
+ cardComplete2.id,
+ cardBadgeText.id,
+ cardMetadata.id,
+ ]);
+ });
+
+ it('should return the correct completed state', () => {
+ expect(renderResult.result.current.isCardComplete(cardComplete.id)).toEqual(false);
+ });
+
+ it('should not report the completed card', async () => {
+ expect(mockReportCardComplete).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when a the auto check is called', () => {
+ beforeEach(async () => {
+ jest.clearAllMocks();
+ cardIncomplete.checkComplete.mockResolvedValueOnce(true);
+ await act(async () => {
+ renderResult.result.current.checkCardComplete(cardIncomplete.id);
+ await waitFor(() => {
+ expect(mockSetStoredCompletedCardIds).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+
+ it('should set the correct completed card ids', async () => {
+ expect(mockSetStoredCompletedCardIds).toHaveBeenCalledWith([
+ cardComplete.id,
+ cardComplete2.id,
+ cardBadgeText.id,
+ cardMetadata.id,
+ cardIncomplete.id,
+ ]);
+ });
+
+ it('should return the correct completed state', () => {
+ expect(renderResult.result.current.isCardComplete(cardIncomplete.id)).toEqual(true);
+ });
+
+ it('should report the completed card', async () => {
+ expect(mockReportCardComplete).toHaveBeenCalledTimes(1);
+ expect(mockReportCardComplete).toHaveBeenCalledWith(cardIncomplete.id, { auto: true });
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_completed_cards.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_completed_cards.ts
new file mode 100644
index 0000000000000..6d8b22c504be9
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_completed_cards.ts
@@ -0,0 +1,144 @@
+/*
+ * 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 { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { useKibana } from '../../../../common/lib/kibana';
+import { useStoredCompletedCardIds } from '../../../hooks/use_stored_state';
+import type { OnboardingCardId } from '../../../constants';
+import type {
+ CheckCompleteResult,
+ CheckCompleteResponse,
+ OnboardingGroupConfig,
+ OnboardingCardConfig,
+} from '../../../types';
+import { useOnboardingContext } from '../../onboarding_context';
+
+export type IsCardComplete = (cardId: OnboardingCardId) => boolean;
+export type SetCardComplete = (
+ cardId: OnboardingCardId,
+ completed: boolean,
+ options?: { auto?: boolean }
+) => void;
+export type GetCardCheckCompleteResult = (
+ cardId: OnboardingCardId
+) => CheckCompleteResult | undefined;
+
+export type CardCheckCompleteResult = Partial>;
+
+/**
+ * This hook implements the logic for tracking which onboarding cards have been completed using Local Storage.
+ */
+export const useCompletedCards = (cardsGroupConfig: OnboardingGroupConfig[]) => {
+ const { spaceId, reportCardComplete } = useOnboardingContext();
+ const services = useKibana().services;
+ const autoCheckCompletedRef = useRef(false);
+
+ // Use stored state to keep localStorage in sync, and a local state to avoid unnecessary re-renders.
+ const [storedCompleteCardIds, setStoredCompleteCardIds] = useStoredCompletedCardIds(spaceId);
+ const [completeCardIds, setCompleteCardIds] = useState(storedCompleteCardIds);
+ // Local state to store the checkCompleteResult for each card
+ const [cardCheckCompleteResult, setCardsCompleteResult] = useState({});
+
+ const isCardComplete = useCallback(
+ (cardId) => completeCardIds.includes(cardId),
+ [completeCardIds]
+ );
+
+ const setCardComplete = useCallback(
+ (cardId, completed, options) => {
+ // This state update has side effects, using a callback
+ setCompleteCardIds((currentCompleteCards) => {
+ const isCurrentlyComplete = currentCompleteCards.includes(cardId);
+ if (completed && !isCurrentlyComplete) {
+ const newCompleteCardIds = [...currentCompleteCards, cardId];
+ reportCardComplete(cardId, options);
+ setStoredCompleteCardIds(newCompleteCardIds); // Keep the stored state in sync with the local state
+ return newCompleteCardIds;
+ } else if (!completed && isCurrentlyComplete) {
+ const newCompleteCardIds = currentCompleteCards.filter((id) => id !== cardId);
+ setStoredCompleteCardIds(newCompleteCardIds); // Keep the stored state in sync with the local state
+ return newCompleteCardIds;
+ }
+ return currentCompleteCards; // No change
+ });
+ },
+ [reportCardComplete, setStoredCompleteCardIds] // static dependencies, this function needs to be stable
+ );
+
+ const getCardCheckCompleteResult = useCallback(
+ (cardId) => cardCheckCompleteResult[cardId],
+ [cardCheckCompleteResult]
+ );
+
+ // Internal: sets the checkCompleteResult for a specific card
+ const setCardCheckCompleteResult = useCallback(
+ (cardId: OnboardingCardId, options: CheckCompleteResult) => {
+ setCardsCompleteResult((currentCardCheckCompleteResult = {}) => ({
+ ...currentCardCheckCompleteResult,
+ [cardId]: options,
+ }));
+ },
+ []
+ );
+
+ // Internal: stores all cards that have a checkComplete function in a flat array
+ const cardsWithAutoCheck = useMemo(
+ () =>
+ cardsGroupConfig.reduce((acc, group) => {
+ acc.push(...group.cards.filter((card) => card.checkComplete));
+ return acc;
+ }, []),
+ [cardsGroupConfig]
+ );
+
+ // Internal: sets the result of a checkComplete function
+ const processCardCheckCompleteResult = useCallback(
+ (cardId: OnboardingCardId, checkCompleteResult: CheckCompleteResponse) => {
+ if (typeof checkCompleteResult === 'boolean') {
+ setCardComplete(cardId, checkCompleteResult, { auto: true });
+ } else {
+ const { isComplete, ...result } = checkCompleteResult;
+ setCardComplete(cardId, isComplete, { auto: true });
+ setCardCheckCompleteResult(cardId, result);
+ }
+ },
+ [setCardComplete, setCardCheckCompleteResult]
+ );
+
+ const checkCardComplete = useCallback(
+ (cardId: OnboardingCardId) => {
+ const cardConfig = cardsWithAutoCheck.find(({ id }) => id === cardId);
+
+ if (cardConfig) {
+ cardConfig.checkComplete?.(services).then((checkCompleteResult) => {
+ processCardCheckCompleteResult(cardId, checkCompleteResult);
+ });
+ }
+ },
+ [cardsWithAutoCheck, processCardCheckCompleteResult, services]
+ );
+
+ useEffect(() => {
+ // Initial auto-check for all cards, it should run only once, after cardsGroupConfig is properly populated
+ if (cardsWithAutoCheck.length === 0 || autoCheckCompletedRef.current) {
+ return;
+ }
+ autoCheckCompletedRef.current = true;
+ cardsWithAutoCheck.map((card) =>
+ card.checkComplete?.(services).then((checkCompleteResult) => {
+ processCardCheckCompleteResult(card.id, checkCompleteResult);
+ })
+ );
+ }, [cardsWithAutoCheck, processCardCheckCompleteResult, services]);
+
+ return {
+ isCardComplete,
+ setCardComplete,
+ getCardCheckCompleteResult,
+ checkCardComplete,
+ };
+};
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_expanded_card.test.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_expanded_card.test.ts
new file mode 100644
index 0000000000000..55f60e591c17d
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_expanded_card.test.ts
@@ -0,0 +1,170 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { renderHook, act } from '@testing-library/react-hooks';
+import { useExpandedCard } from './use_expanded_card';
+import { HEIGHT_ANIMATION_DURATION } from '../onboarding_card_panel.styles';
+import type { OnboardingCardId } from '../../../constants';
+import { mockReportCardOpen } from '../../__mocks__/onboarding_context_mocks';
+import { waitFor } from '@testing-library/react';
+
+const scrollTimeout = HEIGHT_ANIMATION_DURATION + 50;
+
+const mockSetStorageExpandedCardId = jest.fn();
+const mockUseStoredExpandedCardId = jest.fn(() => [null, mockSetStorageExpandedCardId]);
+jest.mock('../../../hooks/use_stored_state', () => ({
+ ...jest.requireActual('../../../hooks/use_stored_state'),
+ useStoredExpandedCardId: () => mockUseStoredExpandedCardId(),
+}));
+
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useLocation: () => ({ hash: '#card-1', pathname: '/test' }),
+}));
+
+jest.mock('../../onboarding_context');
+
+describe('useExpandedCard Hook', () => {
+ const mockCardId = 'card-1' as OnboardingCardId;
+ const mockScrollTo = jest.fn();
+ global.window.scrollTo = mockScrollTo;
+ const mockReplaceState = jest.fn();
+ global.history.replaceState = mockReplaceState;
+
+ const mockGetElementById = jest.fn().mockReturnValue({
+ focus: jest.fn(),
+ offsetTop: 100,
+ });
+ document.getElementById = mockGetElementById;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('when the page is loading', () => {
+ beforeEach(() => {
+ Object.defineProperty(document, 'readyState', {
+ value: 'loading',
+ configurable: true,
+ });
+ });
+
+ it('should not scroll if the page is not fully loaded', async () => {
+ renderHook(useExpandedCard);
+
+ // Ensure that scroll and focus were triggered
+ await waitFor(
+ () => {
+ expect(mockScrollTo).not.toHaveBeenCalled();
+ },
+ { timeout: scrollTimeout }
+ );
+ });
+ });
+
+ describe('when the page is completely loaded', () => {
+ beforeEach(() => {
+ Object.defineProperty(document, 'readyState', {
+ value: 'complete',
+ configurable: true,
+ });
+ renderHook(useExpandedCard);
+ });
+
+ it('should set the expanded card id from the hash', () => {
+ expect(mockSetStorageExpandedCardId).toHaveBeenCalledWith(mockCardId);
+ });
+
+ it('should scroll to the expanded card id from the hash', async () => {
+ // Ensure that scroll and focus were triggered
+ await waitFor(
+ () => {
+ expect(mockGetElementById).toHaveBeenCalledWith(mockCardId);
+ expect(mockScrollTo).toHaveBeenCalledWith({ top: 60, behavior: 'smooth' });
+ },
+ { timeout: scrollTimeout }
+ );
+ });
+
+ it('should report the expanded card id from the hash', () => {
+ expect(mockReportCardOpen).toHaveBeenCalledWith(mockCardId, { auto: true });
+ });
+ });
+
+ describe('when the card is expanded manually', () => {
+ beforeEach(() => {
+ mockGetElementById.mockReturnValueOnce({
+ focus: jest.fn(),
+ offsetTop: 200,
+ });
+ });
+
+ describe('when scroll is disabled', () => {
+ beforeEach(() => {
+ const { result } = renderHook(useExpandedCard);
+ act(() => {
+ result.current.setExpandedCardId(mockCardId, { scroll: false });
+ });
+ });
+
+ it('should set the expanded card id in storage', () => {
+ expect(mockSetStorageExpandedCardId).toHaveBeenCalledWith(mockCardId);
+ });
+
+ it('should set the URL hash', () => {
+ expect(mockReplaceState).toHaveBeenCalledWith(null, '', `#${mockCardId}`);
+ });
+
+ it('should not scroll', async () => {
+ // Ensure that scroll and focus were triggered
+ await waitFor(
+ () => {
+ expect(mockGetElementById).not.toHaveBeenCalled();
+ expect(mockScrollTo).not.toHaveBeenCalled();
+ },
+ { timeout: scrollTimeout }
+ );
+ });
+
+ it('should report the expanded card id', () => {
+ expect(mockReportCardOpen).toHaveBeenCalledWith(mockCardId);
+ });
+ });
+
+ describe('when scroll is enabled', () => {
+ beforeEach(() => {
+ const { result } = renderHook(useExpandedCard);
+ act(() => {
+ result.current.setExpandedCardId(mockCardId, { scroll: true });
+ });
+ });
+
+ it('should set the expanded card id in storage', () => {
+ expect(mockSetStorageExpandedCardId).toHaveBeenCalledWith(mockCardId);
+ });
+
+ it('should set the URL hash', () => {
+ expect(mockReplaceState).toHaveBeenCalledWith(null, '', `#${mockCardId}`);
+ });
+
+ it('should scroll', async () => {
+ // Ensure that scroll and focus were triggered
+ await waitFor(
+ () => {
+ expect(mockGetElementById).toHaveBeenCalledWith(mockCardId);
+ expect(mockScrollTo).toHaveBeenCalledWith({ top: 160, behavior: 'smooth' });
+ },
+ { timeout: scrollTimeout }
+ );
+ });
+
+ it('should report the expanded card id', () => {
+ expect(mockReportCardOpen).toHaveBeenCalledWith(mockCardId);
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_expanded_card.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_expanded_card.ts
new file mode 100644
index 0000000000000..131953e4b0687
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/hooks/use_expanded_card.ts
@@ -0,0 +1,83 @@
+/*
+ * 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 { useCallback, useEffect, useState } from 'react';
+import { useLocation } from 'react-router-dom';
+import { useStoredExpandedCardId } from '../../../hooks/use_stored_state';
+import { HEIGHT_ANIMATION_DURATION } from '../onboarding_card_panel.styles';
+import type { OnboardingCardId } from '../../../constants';
+import type { SetExpandedCardId } from '../../../types';
+import { useOnboardingContext } from '../../onboarding_context';
+
+const HEADER_OFFSET = 40;
+
+const scrollToCard = (cardId: OnboardingCardId) => {
+ setTimeout(() => {
+ const element = document.getElementById(cardId);
+ if (element) {
+ element.focus({ preventScroll: true });
+ window.scrollTo({ top: element.offsetTop - HEADER_OFFSET, behavior: 'smooth' });
+ }
+ }, HEIGHT_ANIMATION_DURATION);
+};
+
+const setHash = (cardId: OnboardingCardId | null) => {
+ history.replaceState(null, '', cardId == null ? ' ' : `#${cardId}`);
+};
+
+/**
+ * This hook manages the expanded card id state in the LocalStorage and the hash in the URL.
+ */
+export const useExpandedCard = () => {
+ const { spaceId, reportCardOpen } = useOnboardingContext();
+ const [expandedCardId, setStorageExpandedCardId] = useStoredExpandedCardId(spaceId);
+ const location = useLocation();
+
+ const [documentReadyState, setReadyState] = useState(document.readyState);
+
+ useEffect(() => {
+ const readyStateListener = () => setReadyState(document.readyState);
+ document.addEventListener('readystatechange', readyStateListener);
+ return () => document.removeEventListener('readystatechange', readyStateListener);
+ }, []);
+
+ // This effect implements auto-scroll in the initial render, further changes in the hash should not trigger this effect
+ useEffect(() => {
+ if (documentReadyState !== 'complete') return; // Wait for page to finish loading before scrolling
+ let cardIdFromHash = location.hash.split('?')[0].replace('#', '') as OnboardingCardId | '';
+ if (!cardIdFromHash) {
+ if (expandedCardId == null) return;
+ // If the hash is empty, but it is defined the storage we use the storage value
+ cardIdFromHash = expandedCardId;
+ setHash(cardIdFromHash);
+ }
+
+ // If the hash is defined and different from the storage, the hash takes precedence
+ if (expandedCardId !== cardIdFromHash) {
+ setStorageExpandedCardId(cardIdFromHash);
+ reportCardOpen(cardIdFromHash, { auto: true });
+ }
+ scrollToCard(cardIdFromHash);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [documentReadyState]);
+
+ const setExpandedCardId = useCallback(
+ (cardId, options) => {
+ setStorageExpandedCardId(cardId);
+ setHash(cardId);
+ if (cardId != null) {
+ reportCardOpen(cardId);
+ if (options?.scroll) {
+ scrollToCard(cardId);
+ }
+ }
+ },
+ [setStorageExpandedCardId, reportCardOpen]
+ );
+
+ return { expandedCardId, setExpandedCardId };
+};
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/hooks/__mocks__/use_projects_url.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/index.ts
similarity index 79%
rename from x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/hooks/__mocks__/use_projects_url.ts
rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/index.ts
index d7e75e6bda2aa..ccbe527f38ba8 100644
--- a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/hooks/__mocks__/use_projects_url.ts
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/index.ts
@@ -5,4 +5,4 @@
* 2.0.
*/
-export const useProjectsUrl = jest.fn(() => 'mock_projects_url');
+export { OnboardingBody } from './onboarding_body';
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.test.tsx
new file mode 100644
index 0000000000000..0b4aca3a9f6e6
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.test.tsx
@@ -0,0 +1,139 @@
+/*
+ * 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 { render, screen, fireEvent, act } from '@testing-library/react';
+import { OnboardingBody } from './onboarding_body';
+import { useBodyConfig } from './hooks/use_body_config';
+import { useExpandedCard } from './hooks/use_expanded_card';
+import { useCompletedCards } from './hooks/use_completed_cards';
+
+jest.mock('../onboarding_context');
+jest.mock('./hooks/use_body_config');
+jest.mock('./hooks/use_expanded_card');
+jest.mock('./hooks/use_completed_cards');
+
+const mockUseBodyConfig = useBodyConfig as jest.Mock;
+const mockUseExpandedCard = useExpandedCard as jest.Mock;
+const mockUseCompletedCards = useCompletedCards as jest.Mock;
+
+// Mock the hooks to return desired test data
+const mockComponent = jest.fn(function Component(_: { setComplete: (complete: boolean) => void }) {
+ return {'Card 1 Content'}
;
+});
+mockUseBodyConfig.mockReturnValue([
+ {
+ title: 'Group 1',
+ cards: [
+ {
+ id: 'card-1',
+ title: 'Card 1',
+ icon: 'icon1',
+ Component: mockComponent,
+ },
+ ],
+ },
+]);
+
+const mockSetExpandedCardId = jest.fn();
+mockUseExpandedCard.mockReturnValue({
+ expandedCardId: null,
+ setExpandedCardId: mockSetExpandedCardId,
+});
+const mockCheckCardComplete = jest.fn();
+const mockSetCardComplete = jest.fn();
+mockUseCompletedCards.mockReturnValue({
+ isCardComplete: jest.fn(() => false),
+ setCardComplete: mockSetCardComplete,
+ getCardCheckCompleteResult: jest.fn(),
+ checkCardComplete: mockCheckCardComplete,
+});
+
+describe('OnboardingBody Component', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should render the OnboardingBody component with the correct content', () => {
+ render();
+ expect(screen.getByText('Group 1')).toBeInTheDocument();
+ expect(screen.getByText('Card 1')).toBeInTheDocument();
+ });
+
+ describe('when the card is expanded', () => {
+ beforeEach(() => {
+ render();
+ fireEvent.click(screen.getByText('Card 1'));
+ });
+
+ it('should set the expanded card', () => {
+ expect(mockSetExpandedCardId).toHaveBeenCalledWith('card-1');
+ });
+
+ it('should check the card for completion', () => {
+ expect(mockCheckCardComplete).toHaveBeenCalledWith('card-1');
+ });
+ });
+
+ describe('when the card is collapsed', () => {
+ beforeEach(() => {
+ mockUseExpandedCard.mockReturnValueOnce({
+ expandedCardId: 'card-1',
+ setExpandedCardId: mockSetExpandedCardId,
+ });
+
+ render();
+
+ fireEvent.click(screen.getByText('Card 1'));
+ });
+
+ it('should unset the expanded the card', async () => {
+ expect(mockSetExpandedCardId).toHaveBeenCalledWith(null);
+ });
+
+ it('should not check the card for completion', () => {
+ expect(mockCheckCardComplete).not.toHaveBeenCalledWith('card-1');
+ });
+ });
+
+ describe('when the card is set as complete from the card component', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockComponent.mockImplementationOnce(function Component({ setComplete }) {
+ setComplete(true);
+ return {'Card 1 Content'}
;
+ });
+
+ render();
+ act(() => {
+ fireEvent.click(screen.getByText('Card 1'));
+ });
+ });
+
+ it('should set the card as complete', () => {
+ expect(mockSetCardComplete).toHaveBeenCalledWith('card-1', true);
+ });
+ });
+
+ describe('when the card is set as incomplete from the card component', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockComponent.mockImplementationOnce(function Component({ setComplete }) {
+ setComplete(false);
+ return {'Card 1 Content'}
;
+ });
+
+ render();
+ act(() => {
+ fireEvent.click(screen.getByText('Card 1'));
+ });
+ });
+
+ it('should set the card as incomplete', () => {
+ expect(mockSetCardComplete).toHaveBeenCalledWith('card-1', false);
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx
new file mode 100644
index 0000000000000..3209028e1f0cd
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_body.tsx
@@ -0,0 +1,92 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { Suspense, useCallback } from 'react';
+import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
+import { CenteredLoadingSpinner } from '../../../common/components/centered_loading_spinner';
+import type { OnboardingCardId } from '../../constants';
+import { useBodyConfig } from './hooks/use_body_config';
+import { OnboardingCardGroup } from './onboarding_card_group';
+import { OnboardingCardPanel } from './onboarding_card_panel';
+import { useExpandedCard } from './hooks/use_expanded_card';
+import { useCompletedCards } from './hooks/use_completed_cards';
+
+export const OnboardingBody = React.memo(() => {
+ const bodyConfig = useBodyConfig();
+
+ const { expandedCardId, setExpandedCardId } = useExpandedCard();
+ const { isCardComplete, setCardComplete, getCardCheckCompleteResult, checkCardComplete } =
+ useCompletedCards(bodyConfig);
+
+ const createOnToggleExpanded = useCallback(
+ (cardId: OnboardingCardId) => () => {
+ if (expandedCardId === cardId) {
+ setExpandedCardId(null);
+ } else {
+ setExpandedCardId(cardId);
+ checkCardComplete(cardId);
+ }
+ },
+ [expandedCardId, setExpandedCardId, checkCardComplete]
+ );
+
+ const createSetCardComplete = useCallback(
+ (cardId: OnboardingCardId) => (complete: boolean) => {
+ setCardComplete(cardId, complete);
+ },
+ [setCardComplete]
+ );
+
+ const createCheckCardComplete = useCallback(
+ (cardId: OnboardingCardId) => () => {
+ checkCardComplete(cardId);
+ },
+ [checkCardComplete]
+ );
+
+ return (
+
+ {bodyConfig.map((group, index) => (
+
+
+
+
+ {group.cards.map(({ id, title, icon, Component: LazyCardComponent }) => {
+ const cardCheckCompleteResult = getCardCheckCompleteResult(id);
+ return (
+
+
+ }>
+
+
+
+
+ );
+ })}
+
+
+
+ ))}
+
+ );
+});
+
+OnboardingBody.displayName = 'OnboardingBody';
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_group.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_group.tsx
new file mode 100644
index 0000000000000..801c8f2e2aa8f
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_group.tsx
@@ -0,0 +1,24 @@
+/*
+ * 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, { type PropsWithChildren } from 'react';
+import { EuiSpacer, EuiTitle } from '@elastic/eui';
+
+export const OnboardingCardGroup = React.memo>(
+ ({ title, children }) => {
+ return (
+
+
+ {title}
+
+
+ {children}
+
+ );
+ }
+);
+OnboardingCardGroup.displayName = 'OnboardingCardGroup';
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.styles.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.styles.ts
new file mode 100644
index 0000000000000..a01d80b27488f
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.styles.ts
@@ -0,0 +1,67 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { COLOR_MODES_STANDARD, useEuiBackgroundColor, useEuiTheme } from '@elastic/eui';
+import { css } from '@emotion/css';
+
+export const HEIGHT_ANIMATION_DURATION = 250;
+
+export const useCardPanelStyles = () => {
+ const { euiTheme, colorMode } = useEuiTheme();
+ const successBackgroundColor = useEuiBackgroundColor('success');
+ const isDarkMode = colorMode === COLOR_MODES_STANDARD.dark;
+
+ return css`
+ .onboardingCardHeader {
+ padding: calc(${euiTheme.size.s} * 2);
+ cursor: pointer;
+ }
+ .onboardingCardIcon {
+ padding: ${euiTheme.size.m};
+ border-radius: 50%;
+ background-color: ${isDarkMode ? euiTheme.colors.body : euiTheme.colors.lightestShade};
+ }
+ .onboardingCardHeaderTitle {
+ font-weight: ${euiTheme.font.weight.semiBold};
+ }
+ .onboardingCardHeaderCompleteBadge {
+ background-color: ${successBackgroundColor};
+ color: ${euiTheme.colors.successText};
+ }
+ .onboardingCardContentWrapper {
+ display: grid;
+ visibility: collapse;
+ grid-template-rows: 0fr;
+ transition: grid-template-rows ${HEIGHT_ANIMATION_DURATION}ms ease-in,
+ visibility ${euiTheme.animation.normal} ${euiTheme.animation.resistance};
+ }
+ .onboardingCardContent {
+ overflow: hidden;
+ }
+
+ &.onboardingCardPanel-expanded {
+ border: 2px solid ${euiTheme.colors.primary};
+
+ .onboardingCardContentWrapper {
+ visibility: visible;
+ grid-template-rows: 1fr;
+ }
+ }
+
+ &.onboardingCardPanel-completed {
+ .onboardingCardIcon {
+ background-color: ${successBackgroundColor};
+ }
+ }
+ ${isDarkMode
+ ? `
+ background-color: ${euiTheme.colors.lightestShade};
+ border: 1px solid ${euiTheme.colors.mediumShade};
+ `
+ : ''}
+ `;
+};
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.test.tsx
new file mode 100644
index 0000000000000..051b6f113f4f1
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.test.tsx
@@ -0,0 +1,97 @@
+/*
+ * 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 { render, screen, fireEvent } from '@testing-library/react';
+import { OnboardingCardPanel } from './onboarding_card_panel';
+import { CARD_COMPLETE_BADGE, EXPAND_CARD_BUTTON_LABEL } from './translations';
+import type { OnboardingCardId } from '../../constants';
+
+describe('OnboardingCardPanel Component', () => {
+ const defaultProps = {
+ id: 'card-1' as OnboardingCardId,
+ title: 'Test Card',
+ icon: 'testIcon',
+ isExpanded: false,
+ isComplete: false,
+ onToggleExpanded: jest.fn(),
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders the card title', () => {
+ render(
+
+ {'Test Card Content'}
+
+ );
+
+ // Verify that the card title and icon are rendered
+ expect(screen.getByText('Test Card')).toBeInTheDocument();
+ });
+
+ it('displays the complete badge if the card is complete', () => {
+ render(
+
+ {'Test Card Content'}
+
+ );
+
+ // Verify that the complete badge is displayed
+ expect(screen.getByText(CARD_COMPLETE_BADGE)).toBeInTheDocument();
+ });
+
+ it('does not display the complete badge if the card is not complete', () => {
+ render(
+
+ {'Test Card Content'}
+
+ );
+
+ // Verify that the complete badge is not displayed
+ expect(screen.queryByText(CARD_COMPLETE_BADGE)).not.toBeInTheDocument();
+ });
+
+ it('calls onToggleExpanded when clicking on the card header', () => {
+ render(
+
+ {'Test Card Content'}
+
+ );
+
+ // Click on the card header
+ fireEvent.click(screen.getByText('Test Card'));
+
+ // Ensure that the onToggleExpanded function is called
+ expect(defaultProps.onToggleExpanded).toHaveBeenCalledTimes(1);
+ });
+
+ it('displays the correct button icon based on isExpanded prop', () => {
+ const { rerender } = render(
+
+ {'Test Card Content'}
+
+ );
+
+ // Check the button icon when card is not expanded
+ const buttonIcon = screen.getByLabelText(EXPAND_CARD_BUTTON_LABEL('Test Card'));
+ expect(buttonIcon).toHaveAttribute('aria-expanded', 'false');
+ expect(buttonIcon).toHaveClass('euiButtonIcon'); // EuiButtonIcon should be rendered
+
+ // Re-render the component with the card expanded
+ rerender(
+
+ {'Test Card Content'}
+
+ );
+
+ // Check the button icon when card is expanded
+ expect(buttonIcon).toHaveAttribute('aria-expanded', 'true');
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.tsx
new file mode 100644
index 0000000000000..eaba74a5615ee
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/onboarding_card_panel.tsx
@@ -0,0 +1,111 @@
+/*
+ * 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, { type PropsWithChildren } from 'react';
+import type { IconType } from '@elastic/eui';
+import {
+ EuiPanel,
+ EuiBadge,
+ EuiButtonIcon,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiIcon,
+ EuiTitle,
+} from '@elastic/eui';
+import classnames from 'classnames';
+import type { OnboardingCardId } from '../../constants';
+import type { CheckCompleteResult } from '../../types';
+import { CARD_COMPLETE_BADGE, EXPAND_CARD_BUTTON_LABEL } from './translations';
+import { useCardPanelStyles } from './onboarding_card_panel.styles';
+
+interface OnboardingCardPanelProps {
+ id: OnboardingCardId;
+ title: string;
+ icon: IconType;
+ isExpanded: boolean;
+ isComplete: boolean;
+ onToggleExpanded: () => void;
+ checkCompleteResult?: CheckCompleteResult;
+}
+
+export const OnboardingCardPanel = React.memo>(
+ ({
+ id,
+ title,
+ icon,
+ isExpanded,
+ isComplete,
+ onToggleExpanded,
+ checkCompleteResult,
+ children,
+ }) => {
+ const styles = useCardPanelStyles();
+ const cardPanelClassName = classnames(styles, {
+ 'onboardingCardPanel-expanded': isExpanded,
+ 'onboardingCardPanel-completed': isComplete,
+ });
+
+ return (
+
+
+
+
+
+
+
+
+
+ {title}
+
+
+
+ {checkCompleteResult?.additionalBadges?.map((additionalBadge, index) => (
+
+ {additionalBadge}
+
+ )) ?? null}
+
+ {isComplete && (
+
+
+ {checkCompleteResult?.completeBadgeText || CARD_COMPLETE_BADGE}
+
+
+ )}
+
+
+
+
+
+
+
+ );
+ }
+);
+OnboardingCardPanel.displayName = 'OnboardingCardPanel';
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/translations.ts
new file mode 100644
index 0000000000000..c70f592873cbf
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_body/translations.ts
@@ -0,0 +1,21 @@
+/*
+ * 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 CARD_COMPLETE_BADGE = i18n.translate(
+ 'xpack.securitySolution.onboarding.cardComplete',
+ {
+ defaultMessage: 'Completed',
+ }
+);
+
+export const EXPAND_CARD_BUTTON_LABEL = (title: string) =>
+ i18n.translate('xpack.securitySolution.onboarding.expandCardButtonAriaLabel', {
+ defaultMessage: 'Expand "{title}"',
+ values: { title },
+ });
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_context.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_context.tsx
new file mode 100644
index 0000000000000..dda17e18c087e
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_context.tsx
@@ -0,0 +1,62 @@
+/*
+ * 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 { PropsWithChildren } from 'react';
+import React, { createContext, useContext, useMemo } from 'react';
+import { useKibana } from '../../common/lib/kibana/kibana_react';
+import type { OnboardingCardId } from '../constants';
+
+export interface OnboardingContextValue {
+ spaceId: string;
+ reportCardOpen: (cardId: OnboardingCardId, options?: { auto?: boolean }) => void;
+ reportCardComplete: (cardId: OnboardingCardId, options?: { auto?: boolean }) => void;
+ reportCardLinkClicked: (cardId: OnboardingCardId, linkId: string) => void;
+}
+const OnboardingContext = createContext(null);
+
+export const OnboardingContextProvider: React.FC> =
+ React.memo(({ children, spaceId }) => {
+ const { telemetry } = useKibana().services;
+
+ const value = useMemo(
+ () => ({
+ spaceId,
+ reportCardOpen: (cardId, { auto = false } = {}) => {
+ telemetry.reportOnboardingHubStepOpen({
+ stepId: cardId,
+ trigger: auto ? 'navigation' : 'click',
+ });
+ },
+ reportCardComplete: (cardId, { auto = false } = {}) => {
+ telemetry.reportOnboardingHubStepFinished({
+ stepId: cardId,
+ trigger: auto ? 'auto_check' : 'click',
+ });
+ },
+ reportCardLinkClicked: (cardId, linkId: string) => {
+ telemetry.reportOnboardingHubStepLinkClicked({
+ originStepId: cardId,
+ stepLinkId: linkId,
+ });
+ },
+ }),
+ [spaceId, telemetry]
+ );
+
+ return {children};
+ });
+OnboardingContextProvider.displayName = 'OnboardingContextProvider';
+
+export const useOnboardingContext = () => {
+ const context = useContext(OnboardingContext);
+ if (!context) {
+ throw new Error(
+ 'No OnboardingContext found. Please wrap the application with OnboardingProvider'
+ );
+ }
+ return context;
+};
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/footer_items.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/footer_items.ts
new file mode 100644
index 0000000000000..e0e2b272da3aa
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/footer_items.ts
@@ -0,0 +1,81 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { i18n } from '@kbn/i18n';
+import documentation from './images/documentation.png';
+import forum from './images/forum.png';
+import demo from './images/demo.png';
+import labs from './images/labs.png';
+
+export const footerItems = [
+ {
+ icon: documentation,
+ key: 'documentation',
+ title: i18n.translate('xpack.securitySolution.onboarding.footer.documentation.title', {
+ defaultMessage: 'Browse documentation',
+ }),
+ description: i18n.translate(
+ 'xpack.securitySolution.onboarding.footer.documentation.description',
+ {
+ defaultMessage: 'In-depth guides on all Elastic features',
+ }
+ ),
+ link: {
+ title: i18n.translate('xpack.securitySolution.onboarding.footer.documentation.link.title', {
+ defaultMessage: 'Start reading',
+ }),
+ href: 'https://docs.elastic.co/integrations/elastic-security-intro',
+ },
+ },
+ {
+ icon: forum,
+ key: 'forum',
+ title: i18n.translate('xpack.securitySolution.onboarding.footer.forum.title', {
+ defaultMessage: 'Explore forum',
+ }),
+ description: i18n.translate('xpack.securitySolution.onboarding.footer.forum.description', {
+ defaultMessage: 'Exchange thoughts about Elastic',
+ }),
+ link: {
+ title: i18n.translate('xpack.securitySolution.onboarding.footer.forum.link.title', {
+ defaultMessage: 'Discuss Forum',
+ }),
+ href: 'https://discuss.elastic.co/c/security/83',
+ },
+ },
+ {
+ icon: demo,
+ key: 'demo',
+ title: i18n.translate('xpack.securitySolution.onboarding.footer.demo.title', {
+ defaultMessage: 'View demo project',
+ }),
+ description: i18n.translate('xpack.securitySolution.onboarding.footer.demo.description', {
+ defaultMessage: 'Discover Elastic using sample data',
+ }),
+ link: {
+ title: i18n.translate('xpack.securitySolution.onboarding.footer.demo.link.title', {
+ defaultMessage: 'Explore demo',
+ }),
+ href: 'https://www.elastic.co/demo-gallery?solutions=security&features=null',
+ },
+ },
+ {
+ icon: labs,
+ key: 'labs',
+ title: i18n.translate('xpack.securitySolution.onboarding.footer.labs.title', {
+ defaultMessage: 'Elastic Security Labs',
+ }),
+ description: i18n.translate('xpack.securitySolution.onboarding.footer.labs.description', {
+ defaultMessage: 'Insights from security researchers',
+ }),
+ link: {
+ title: i18n.translate('xpack.securitySolution.onboarding.footer.labs.link.title', {
+ defaultMessage: 'Learn more',
+ }),
+ href: 'https://www.elastic.co/security-labs',
+ },
+ },
+] as const;
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/images/demo.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/images/demo.png
similarity index 100%
rename from x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/images/demo.png
rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/images/demo.png
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/images/documentation.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/images/documentation.png
similarity index 100%
rename from x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/images/documentation.png
rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/images/documentation.png
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/images/forum.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/images/forum.png
similarity index 100%
rename from x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/images/forum.png
rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/images/forum.png
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/images/labs.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/images/labs.png
similarity index 100%
rename from x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/images/labs.png
rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/images/labs.png
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/index.ts
new file mode 100644
index 0000000000000..59b3da2bc86ad
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { OnboardingFooter } from './onboarding_footer';
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/styles/current_plan_badge.styles.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.styles.ts
similarity index 51%
rename from x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/styles/current_plan_badge.styles.ts
rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.styles.ts
index 6fee540af502e..4f837c7bef611 100644
--- a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/styles/current_plan_badge.styles.ts
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.styles.ts
@@ -4,23 +4,17 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
+
import { useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/css';
-import { useMemo } from 'react';
-export const useCurrentPlanBadgeStyles = () => {
+export const useFooterStyles = () => {
const { euiTheme } = useEuiTheme();
- const styles = useMemo(() => {
- return {
- wrapperStyles: css({
- fontSize: euiTheme.size.m,
- lineHeight: euiTheme.size.m,
- }),
- textStyles: css({
- textTransform: 'capitalize',
- }),
- };
- }, [euiTheme.size.m]);
+ return css`
+ padding: ${euiTheme.size.m} ${euiTheme.size.l};
- return styles;
+ .footerItemTitle {
+ font-weight: ${euiTheme.font.weight.semiBold};
+ }
+ `;
};
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.tsx
new file mode 100644
index 0000000000000..af0f7bdd6e5ef
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_footer/onboarding_footer.tsx
@@ -0,0 +1,37 @@
+/*
+ * 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 { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
+import { useFooterStyles } from './onboarding_footer.styles';
+import { footerItems } from './footer_items';
+
+export const OnboardingFooter = React.memo(() => {
+ const styles = useFooterStyles();
+ return (
+
+ {footerItems.map((item) => (
+
+
+
+
+ {item.title}
+
+
+ {item.description}
+
+
+
+ {item.link.title}
+
+
+
+ ))}
+
+ );
+});
+OnboardingFooter.displayName = 'OnboardingFooter';
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/data_ingestion_hub_header/cards/card.styles.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.styles.ts
similarity index 66%
rename from x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/data_ingestion_hub_header/cards/card.styles.ts
rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.styles.ts
index 377a7b73efe5c..8643c3254a6ee 100644
--- a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/data_ingestion_hub_header/cards/card.styles.ts
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.styles.ts
@@ -14,17 +14,22 @@ export const useCardStyles = () => {
return css`
min-width: 315px;
+ /* We needed to add the "headerCard" class to make it take priority over the default EUI card styles */
&.headerCard:hover {
- *:not(.headerCardContent) {
+ *:not(.headerCardLink) {
text-decoration: none;
}
- .headerCardContent,
- .headerCardContent * {
+ .headerCardLink,
+ .headerCardLink * {
text-decoration: underline;
text-decoration-color: ${euiTheme.colors.primaryText};
}
}
+ .headerCardTitle {
+ font-weight: ${euiTheme.font.weight.semiBold};
+ }
+
${isDarkMode
? `
background-color: ${euiTheme.colors.lightestShade};
@@ -32,25 +37,5 @@ export const useCardStyles = () => {
border: 1px solid ${euiTheme.colors.mediumShade};
`
: ''}
-
- .headerCardTitle {
- font-size: ${euiTheme.base * 0.875}px;
- font-weight: ${euiTheme.font.weight.semiBold};
- line-height: ${euiTheme.size.l};
- color: ${euiTheme.colors.title};
- text-decoration: none;
- }
-
- .headerCardImage {
- width: 64px;
- height: 64px;
- }
-
- .headerCardDescription {
- font-size: 12.25px;
- font-weight: ${euiTheme.font.weight.regular};
- line-height: ${euiTheme.base * 1.25}px;
- color: ${euiTheme.colors.darkestShade};
- }
`;
};
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/data_ingestion_hub_header/cards/card.test.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.test.tsx
similarity index 71%
rename from x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/data_ingestion_hub_header/cards/card.test.tsx
rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.test.tsx
index 2b0aee4b69019..7499e3feaf5f2 100644
--- a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/data_ingestion_hub_header/cards/card.test.tsx
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.test.tsx
@@ -7,11 +7,7 @@
import React from 'react';
import { render } from '@testing-library/react';
-import { Card } from './card';
-
-jest.mock('../../../../../lib/kibana', () => ({
- useEuiTheme: jest.fn(() => ({ euiTheme: { colorTheme: 'DARK' } })),
-}));
+import { LinkCard } from './link_card';
describe('DataIngestionHubHeaderCardComponent', () => {
beforeEach(() => {
@@ -20,9 +16,12 @@ describe('DataIngestionHubHeaderCardComponent', () => {
it('should render the title, description, and icon', () => {
const { getByTestId, getByText } = render(
-
- {'test'}
-
+
);
expect(getByText('Mock Title')).toBeInTheDocument();
@@ -32,9 +31,12 @@ describe('DataIngestionHubHeaderCardComponent', () => {
it('should apply dark mode styles when color mode is DARK', () => {
const { container } = render(
-
- {'test'}
-
+
);
const cardElement = container.querySelector('.euiCard');
expect(cardElement).toHaveStyle('background-color:rgb(255, 255, 255)');
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.tsx
new file mode 100644
index 0000000000000..95ce9fbaf05b5
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/common/link_card.tsx
@@ -0,0 +1,62 @@
+/*
+ * 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 { EuiCard, EuiImage, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
+import classNames from 'classnames';
+import { useCardStyles } from './link_card.styles';
+
+interface LinkCardProps {
+ icon: string;
+ title: string;
+ description: string;
+ linkText: string;
+ onClick?: () => void;
+ href?: string;
+ target?: string;
+}
+
+export const LinkCard: React.FC = React.memo(
+ ({ icon, title, description, onClick, href, target, linkText }) => {
+ const cardStyles = useCardStyles();
+ const cardClassName = classNames(cardStyles, 'headerCard');
+ return (
+
+ }
+ title={
+
+ {title}
+
+ }
+ description={{description}}
+ >
+
+
+ {/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
+
+ {linkText}
+
+
+
+ );
+ }
+);
+
+LinkCard.displayName = 'LinkCard';
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/demo_card/demo_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/demo_card/demo_card.tsx
new file mode 100644
index 0000000000000..dfb8da662060e
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/demo_card/demo_card.tsx
@@ -0,0 +1,26 @@
+/*
+ * 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 { LinkCard } from '../common/link_card';
+import demoImage from './images/demo_card.png';
+import darkDemoImage from './images/demo_card_dark.png';
+import * as i18n from './translations';
+
+export const DemoCard = React.memo<{ isDarkMode: boolean }>(({ isDarkMode }) => {
+ return (
+
+ );
+});
+DemoCard.displayName = 'DemoCard';
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/images/data_ingestion_hub_demo.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/demo_card/images/demo_card.png
similarity index 100%
rename from x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/images/data_ingestion_hub_demo.png
rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/demo_card/images/demo_card.png
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/images/dark_data_ingestion_hub_demo.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/demo_card/images/demo_card_dark.png
similarity index 100%
rename from x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/images/dark_data_ingestion_hub_demo.png
rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/demo_card/images/demo_card_dark.png
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/demo_card/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/demo_card/index.ts
new file mode 100644
index 0000000000000..78dea5ca094c8
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/demo_card/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { DemoCard } from './demo_card';
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/demo_card/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/demo_card/translations.ts
new file mode 100644
index 0000000000000..3082a1c197ee3
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/demo_card/translations.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const ONBOARDING_HEADER_DEMO_TITLE = i18n.translate(
+ 'xpack.securitySolution.onboarding.header.card.demo.title',
+ {
+ defaultMessage: 'See Elastic Security in action',
+ }
+);
+
+export const ONBOARDING_HEADER_DEMO_DESCRIPTION = i18n.translate(
+ 'xpack.securitySolution.onboarding.header.card.demo.description',
+ {
+ defaultMessage: 'Explore the demo, no setup required!',
+ }
+);
+
+export const ONBOARDING_HEADER_DEMO_LINK_TITLE = i18n.translate(
+ 'xpack.securitySolution.onboarding.header.card.demo.link.title',
+ {
+ defaultMessage: 'Explore Demo',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/images/data_ingestion_hub_teammates.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/teammates_card/images/teammates_card.png
similarity index 100%
rename from x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/images/data_ingestion_hub_teammates.png
rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/teammates_card/images/teammates_card.png
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/images/dark_data_ingestion_hub_teammates.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/teammates_card/images/teammates_card_dark.png
similarity index 100%
rename from x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/images/dark_data_ingestion_hub_teammates.png
rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/teammates_card/images/teammates_card_dark.png
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/teammates_card/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/teammates_card/index.ts
new file mode 100644
index 0000000000000..dde87215dbddb
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/teammates_card/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { TeammatesCard } from './teammates_card';
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/teammates_card/teammates_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/teammates_card/teammates_card.tsx
new file mode 100644
index 0000000000000..da316e0d0d907
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/teammates_card/teammates_card.tsx
@@ -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 { useObservable } from 'react-use';
+import { useOnboardingService } from '../../../../hooks/use_onboarding_service';
+import { LinkCard } from '../common/link_card';
+import teammatesImage from './images/teammates_card.png';
+import darkTeammatesImage from './images/teammates_card_dark.png';
+import * as i18n from './translations';
+
+export const TeammatesCard = React.memo<{ isDarkMode: boolean }>(({ isDarkMode }) => {
+ const { usersUrl$ } = useOnboardingService();
+ const usersUrl = useObservable(usersUrl$, undefined);
+ return (
+
+ );
+});
+TeammatesCard.displayName = 'TeammatesCard';
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/teammates_card/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/teammates_card/translations.ts
new file mode 100644
index 0000000000000..721807721c254
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/teammates_card/translations.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const ONBOARDING_HEADER_TEAMMATES_TITLE = i18n.translate(
+ 'xpack.securitySolution.onboarding.header.card.teammates.title',
+ {
+ defaultMessage: 'Add teammates',
+ }
+);
+
+export const ONBOARDING_HEADER_TEAMMATES_DESCRIPTION = i18n.translate(
+ 'xpack.securitySolution.onboarding.header.card.teammates.description',
+ {
+ defaultMessage: 'Increase collaboration across your org',
+ }
+);
+
+export const ONBOARDING_HEADER_TEAMMATES_LINK_TITLE = i18n.translate(
+ 'xpack.securitySolution.onboarding.header.card.teammates.link.title',
+ {
+ defaultMessage: 'Add users',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/images/data_ingestion_hub_video.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/images/video_card.png
similarity index 100%
rename from x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/images/data_ingestion_hub_video.png
rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/images/video_card.png
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/images/dark_data_ingestion_hub_video.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/images/video_card_dark.png
similarity index 100%
rename from x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/images/dark_data_ingestion_hub_video.png
rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/images/video_card_dark.png
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/index.ts
new file mode 100644
index 0000000000000..aa3018a3b032e
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { VideoCard } from './video_card';
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/translations.ts
new file mode 100644
index 0000000000000..373f587fe84a7
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/translations.ts
@@ -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 { i18n } from '@kbn/i18n';
+
+export const ONBOARDING_HEADER_VIDEO_TITLE = i18n.translate(
+ 'xpack.securitySolution.onboarding.header.card.video.title',
+ {
+ defaultMessage: 'Watch 2 minute overview video',
+ }
+);
+
+export const ONBOARDING_HEADER_VIDEO_DESCRIPTION = i18n.translate(
+ 'xpack.securitySolution.onboarding.header.card.video.description',
+ {
+ defaultMessage: 'Get acquainted with Elastic Security',
+ }
+);
+
+export const ONBOARDING_HEADER_VIDEO_LINK_TITLE = i18n.translate(
+ 'xpack.securitySolution.onboarding.header.card.video.link.title',
+ {
+ defaultMessage: 'Watch video',
+ }
+);
+
+export const ONBOARDING_HEADER_VIDEO_MODAL_TITLE = i18n.translate(
+ 'xpack.securitySolution.onboarding.header.card.videoModal.title',
+ {
+ defaultMessage: 'Welcome to Elastic Security!',
+ }
+);
+
+export const ONBOARDING_HEADER_VIDEO_MODAL_DESCRIPTION = i18n.translate(
+ 'xpack.securitySolution.onboarding.header.card.videoModal.description',
+ {
+ defaultMessage:
+ "We're excited to support you in protecting your organization's data. Here's a preview of the steps you'll take to set up.",
+ }
+);
+
+export const ONBOARDING_HEADER_VIDEO_MODAL_BUTTON_CLOSE = i18n.translate(
+ 'xpack.securitySolution.onboarding.header.card.videoModal.buttonClose',
+ {
+ defaultMessage: 'Close',
+ }
+);
+
+export const ONBOARDING_HEADER_VIDEO_MODAL_VIDEO_TITLE = i18n.translate(
+ 'xpack.securitySolution.onboarding.header.card.videoModal.viewTitle',
+ {
+ defaultMessage: 'Elastic Security',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/video_card.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/video_card.tsx
new file mode 100644
index 0000000000000..8a79f79b9eaea
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/video_card.tsx
@@ -0,0 +1,37 @@
+/*
+ * 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, { useCallback, useState } from 'react';
+import { OnboardingHeaderVideoModal } from './video_modal';
+import * as i18n from './translations';
+import videoImage from './images/video_card.png';
+import darkVideoImage from './images/video_card_dark.png';
+import { LinkCard } from '../common/link_card';
+
+export const VideoCard = React.memo<{ isDarkMode: boolean }>(({ isDarkMode }) => {
+ const [isModalVisible, setIsModalVisible] = useState(false);
+ const closeVideoModal = useCallback(() => {
+ setIsModalVisible(false);
+ }, []);
+ const showVideoModal = useCallback(() => {
+ setIsModalVisible(true);
+ }, []);
+
+ return (
+ <>
+
+ {isModalVisible && }
+ >
+ );
+});
+VideoCard.displayName = 'VideoCard';
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/video_modal.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/video_modal.tsx
new file mode 100644
index 0000000000000..41186988c5e88
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/cards/video_card/video_modal.tsx
@@ -0,0 +1,83 @@
+/*
+ * 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 {
+ EuiButton,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiModal,
+ EuiModalBody,
+ EuiModalFooter,
+ EuiModalHeaderTitle,
+ EuiText,
+ useGeneratedHtmlId,
+} from '@elastic/eui';
+import { css } from '@emotion/css';
+import { ONBOARDING_VIDEO_SOURCE } from '../../../../../common/constants';
+import * as i18n from './translations';
+
+// Not ideal, but we could not find any other way to remove the padding from the modal body
+const modalStyles = css`
+ .euiModalBody__overflow {
+ overflow: hidden;
+ padding: 0px;
+ mask-image: none;
+ }
+`;
+
+interface OnboardingHeaderVideoModalProps {
+ onClose: () => void;
+}
+
+export const OnboardingHeaderVideoModal = React.memo(
+ ({ onClose }) => {
+ const modalTitle = useGeneratedHtmlId();
+
+ return (
+
+
+
+
+
+
+
+
+ {i18n.ONBOARDING_HEADER_VIDEO_MODAL_TITLE}
+
+
+
+
+ {i18n.ONBOARDING_HEADER_VIDEO_MODAL_DESCRIPTION}
+
+
+
+
+ {i18n.ONBOARDING_HEADER_VIDEO_MODAL_BUTTON_CLOSE}
+
+
+
+
+
+ );
+ }
+);
+OnboardingHeaderVideoModal.displayName = 'DataIngestionHubVideoModal';
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/images/rocket.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/images/header_rocket.png
similarity index 100%
rename from x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/images/rocket.png
rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/images/header_rocket.png
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/images/dark_rocket.png b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/images/header_rocket_dark.png
similarity index 100%
rename from x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/images/dark_rocket.png
rename to x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/images/header_rocket_dark.png
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/index.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/index.ts
new file mode 100644
index 0000000000000..d8777b6557468
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { OnboardingHeader } from './onboarding_header';
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header.styles.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header.styles.ts
new file mode 100644
index 0000000000000..34cc060a97386
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header.styles.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { useEuiTheme } from '@elastic/eui';
+import { css } from '@emotion/css';
+import { PAGE_CONTENT_WIDTH } from '../../constants';
+
+export const useOnboardingHeaderStyles = () => {
+ const { euiTheme } = useEuiTheme();
+ return css`
+ .onboardingHeaderTitleWrapper {
+ width: calc(${PAGE_CONTENT_WIDTH} / 2);
+ }
+ .onboardingHeaderGreetings {
+ color: ${euiTheme.colors.darkShade};
+ }
+ `;
+};
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header.tsx b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header.tsx
new file mode 100644
index 0000000000000..0210c88186a9a
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/onboarding_header.tsx
@@ -0,0 +1,79 @@
+/*
+ * 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 {
+ COLOR_MODES_STANDARD,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiImage,
+ EuiSpacer,
+ EuiText,
+ EuiTitle,
+ useEuiTheme,
+} from '@elastic/eui';
+import { useCurrentUser } from '../../../common/lib/kibana/hooks';
+import { useOnboardingHeaderStyles } from './onboarding_header.styles';
+import rocketImage from './images/header_rocket.png';
+import rocketDarkImage from './images/header_rocket_dark.png';
+import { TeammatesCard } from './cards/teammates_card';
+import { VideoCard } from './cards/video_card';
+import { DemoCard } from './cards/demo_card';
+import * as i18n from './translations';
+
+export const OnboardingHeader = React.memo(() => {
+ const currentUser = useCurrentUser();
+ const { colorMode } = useEuiTheme();
+ const isDarkMode = colorMode === COLOR_MODES_STANDARD.dark;
+
+ const styles = useOnboardingHeaderStyles();
+
+ // Full name could be null, user name should always exist
+ const currentUserName = currentUser?.fullName || currentUser?.username;
+
+ return (
+ <>
+
+
+
+
+
+ {currentUserName && (
+
+ {i18n.GET_STARTED_PAGE_TITLE(currentUserName)}
+
+ )}
+
+
+ {i18n.GET_STARTED_DATA_INGESTION_HUB_SUBTITLE}
+
+
+
+ {i18n.GET_STARTED_DATA_INGESTION_HUB_DESCRIPTION}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+});
+OnboardingHeader.displayName = 'OnboardingHeader';
diff --git a/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/translations.ts b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/translations.ts
new file mode 100644
index 0000000000000..c1f8ca8695bb6
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/components/onboarding_header/translations.ts
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const GET_STARTED_PAGE_TITLE = (userName: string) =>
+ i18n.translate('xpack.securitySolution.onboarding.Title', {
+ defaultMessage: `Hi {userName}!`,
+ values: { userName },
+ });
+
+export const GET_STARTED_DATA_INGESTION_HUB_SUBTITLE = i18n.translate(
+ 'xpack.securitySolution.onboarding.subTitle',
+ {
+ defaultMessage: `Welcome to Elastic Security`,
+ }
+);
+
+export const GET_STARTED_DATA_INGESTION_HUB_DESCRIPTION = i18n.translate(
+ 'xpack.securitySolution.onboarding.description',
+ {
+ defaultMessage: `A SIEM with AI-driven security analytics, XDR and Cloud Security.`,
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/onboarding/constants.ts b/x-pack/plugins/security_solution/public/onboarding/constants.ts
new file mode 100644
index 0000000000000..0eb277bd61875
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/constants.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+export const PAGE_CONTENT_WIDTH = '1150px';
+
+export enum OnboardingCardId {
+ integrations = 'integrations',
+ dashboards = 'dashboards',
+ rules = 'rules',
+ alerts = 'alerts',
+ assistant = 'assistant',
+ attackDiscovery = 'attack_discovery',
+}
diff --git a/x-pack/plugins/security_solution/public/onboarding/hooks/use_onboarding_service.ts b/x-pack/plugins/security_solution/public/onboarding/hooks/use_onboarding_service.ts
new file mode 100644
index 0000000000000..55f3ecb8d4aca
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/hooks/use_onboarding_service.ts
@@ -0,0 +1,10 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { useKibana } from '../../common/lib/kibana/kibana_react';
+
+export const useOnboardingService = () => useKibana().services.onboarding;
diff --git a/x-pack/plugins/security_solution/public/onboarding/hooks/use_stored_state.ts b/x-pack/plugins/security_solution/public/onboarding/hooks/use_stored_state.ts
new file mode 100644
index 0000000000000..c22c8f0f5310c
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/hooks/use_stored_state.ts
@@ -0,0 +1,70 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { useLocalStorage } from 'react-use';
+import type { OnboardingCardId } from '../constants';
+import type { IntegrationTabId } from '../components/onboarding_body/cards/integrations/types';
+
+const LocalStorageKey = {
+ avcBannerDismissed: 'ONBOARDING_HUB.AVC_BANNER_DISMISSED',
+ videoVisited: 'ONBOARDING_HUB.VIDEO_VISITED',
+ completeCards: 'ONBOARDING_HUB.COMPLETE_CARDS',
+ expandedCard: 'ONBOARDING_HUB.EXPANDED_CARD',
+ selectedIntegrationTabId: 'ONBOARDING_HUB.SELECTED_INTEGRATION_TAB_ID',
+ IntegrationSearchTerm: 'ONBOARDING_HUB.INTEGRATION_SEARCH_TERM',
+ IntegrationScrollTop: 'ONBOARDING_HUB.INTEGRATION_SCROLL_TOP',
+} as const;
+
+/**
+ * Wrapper hook for useLocalStorage, but always returns the default value when not defined instead of `undefined`.
+ */
+const useDefinedLocalStorage = (key: string, defaultValue: T) => {
+ const [value, setValue] = useLocalStorage(key, defaultValue);
+ return [value ?? defaultValue, setValue] as const;
+};
+
+/**
+ * Stores the AVC banner dismissed state
+ */
+export const useStoredIsAVCBannerDismissed = () =>
+ useDefinedLocalStorage(LocalStorageKey.avcBannerDismissed, false);
+
+/**
+ * Stores the completed card IDs per space
+ */
+export const useStoredCompletedCardIds = (spaceId: string) =>
+ useDefinedLocalStorage(`${LocalStorageKey.completeCards}.${spaceId}`, []);
+
+/**
+ * Stores the expanded card ID per space
+ */
+export const useStoredExpandedCardId = (spaceId: string) =>
+ useDefinedLocalStorage(
+ `${LocalStorageKey.expandedCard}.${spaceId}`,
+ null
+ );
+
+/**
+ * Stores the selected integration tab ID per space
+ */
+export const useStoredIntegrationTabId = (
+ spaceId: string,
+ defaultSelectedTabId: IntegrationTabId
+) =>
+ useDefinedLocalStorage(
+ `${LocalStorageKey.selectedIntegrationTabId}.${spaceId}`,
+ defaultSelectedTabId
+ );
+
+/**
+ * Stores the integration search term per space
+ */
+export const useStoredIntegrationSearchTerm = (spaceId: string) =>
+ useDefinedLocalStorage(
+ `${LocalStorageKey.IntegrationSearchTerm}.${spaceId}`,
+ null
+ );
diff --git a/x-pack/plugins/security_solution/public/onboarding/index.ts b/x-pack/plugins/security_solution/public/onboarding/index.ts
new file mode 100644
index 0000000000000..b20a2777f6ee7
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/index.ts
@@ -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 Onboarding {
+ public setup() {}
+
+ public start(): SecuritySubPlugin {
+ return {
+ routes,
+ };
+ }
+}
diff --git a/x-pack/plugins/security_solution/public/onboarding/jest.config.js b/x-pack/plugins/security_solution/public/onboarding/jest.config.js
new file mode 100644
index 0000000000000..f5c05b19b495b
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/jest.config.js
@@ -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.
+ */
+
+module.exports = {
+ preset: '@kbn/test',
+ rootDir: '../../../../..',
+ roots: ['/x-pack/plugins/security_solution/public/onboarding'],
+ coverageDirectory:
+ '/target/kibana-coverage/jest/x-pack/plugins/security_solution/public/onboarding',
+ coverageReporters: ['text', 'html'],
+ collectCoverageFrom: [
+ '/x-pack/plugins/security_solution/public/onboarding/**/*.{ts,tsx}',
+ ],
+ moduleNameMapper: require('../../server/__mocks__/module_name_map'),
+};
diff --git a/x-pack/plugins/security_solution/public/onboarding/links.ts b/x-pack/plugins/security_solution/public/onboarding/links.ts
new file mode 100644
index 0000000000000..ea9e603a36fac
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/links.ts
@@ -0,0 +1,27 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { i18n } from '@kbn/i18n';
+import { ONBOARDING_PATH, SecurityPageName, SERVER_APP_ID } from '../../common/constants';
+import { GETTING_STARTED } from '../app/translations';
+import type { LinkItem } from '../common/links/types';
+
+export const onboardingLinks: LinkItem = {
+ id: SecurityPageName.landing,
+ title: GETTING_STARTED,
+ path: ONBOARDING_PATH,
+ capabilities: [`${SERVER_APP_ID}.show`],
+ globalSearchKeywords: [
+ i18n.translate('xpack.securitySolution.appLinks.getStarted', {
+ defaultMessage: 'Getting started',
+ }),
+ ],
+ sideNavIcon: 'launch',
+ sideNavFooter: true,
+ skipUrlState: true,
+ hideTimeline: true,
+};
diff --git a/x-pack/plugins/security_solution/public/onboarding/routes.tsx b/x-pack/plugins/security_solution/public/onboarding/routes.tsx
new file mode 100644
index 0000000000000..b8ac8aba3e90e
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/routes.tsx
@@ -0,0 +1,18 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { ONBOARDING_PATH, SecurityPageName } from '../../common/constants';
+import type { SecuritySubPluginRoutes } from '../app/types';
+import { withSecurityRoutePageWrapper } from '../common/components/security_route_page_wrapper';
+import { OnboardingPage } from './components/onboarding';
+
+export const routes: SecuritySubPluginRoutes = [
+ {
+ path: ONBOARDING_PATH,
+ component: withSecurityRoutePageWrapper(OnboardingPage, SecurityPageName.landing),
+ },
+];
diff --git a/x-pack/plugins/security_solution/public/onboarding/service/index.ts b/x-pack/plugins/security_solution/public/onboarding/service/index.ts
new file mode 100644
index 0000000000000..6811bb8b967d1
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/service/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export { OnboardingService } from './onboarding_service';
diff --git a/x-pack/plugins/security_solution/public/onboarding/service/onboarding_service.ts b/x-pack/plugins/security_solution/public/onboarding/service/onboarding_service.ts
new file mode 100644
index 0000000000000..c894d71996f53
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/service/onboarding_service.ts
@@ -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 { BehaviorSubject, type Observable } from 'rxjs';
+
+type UserUrl = string | undefined;
+type IsAgentlessAvailable = boolean | undefined;
+
+export class OnboardingService {
+ private usersUrlSubject$: BehaviorSubject;
+ public usersUrl$: Observable;
+
+ private isAgentlessAvailableSubject$: BehaviorSubject;
+ public isAgentlessAvailable$: Observable;
+
+ constructor() {
+ this.usersUrlSubject$ = new BehaviorSubject(undefined);
+ this.usersUrl$ = this.usersUrlSubject$.asObservable();
+
+ this.isAgentlessAvailableSubject$ = new BehaviorSubject(undefined);
+ this.isAgentlessAvailable$ = this.isAgentlessAvailableSubject$.asObservable();
+ }
+
+ public setSettings({
+ userUrl,
+ isAgentlessAvailable,
+ }: {
+ userUrl: UserUrl;
+ isAgentlessAvailable: boolean;
+ }) {
+ this.usersUrlSubject$.next(userUrl);
+ this.isAgentlessAvailableSubject$.next(isAgentlessAvailable);
+ }
+}
diff --git a/x-pack/plugins/security_solution/public/onboarding/types.ts b/x-pack/plugins/security_solution/public/onboarding/types.ts
new file mode 100644
index 0000000000000..9dfe1e75596db
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/onboarding/types.ts
@@ -0,0 +1,122 @@
+/*
+ * 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 React from 'react';
+import type { IconType } from '@elastic/eui';
+import type { LicenseType } from '@kbn/licensing-plugin/public';
+
+import type { OnboardingCardId } from './constants';
+import type { RequiredCapabilities } from '../common/lib/capabilities';
+import type { StartServices } from '../types';
+
+export interface CheckCompleteResult {
+ /**
+ * Optional custom badge text replacement for the card complete badge in the card header.
+ */
+ completeBadgeText?: string | React.ReactNode;
+ /**
+ * Optional badges to prepend to the card complete badge in the card header, regardless of completion status.
+ */
+ additionalBadges?: React.ReactNode[];
+ /**
+ * Optional metadata to be passed to the card component.
+ */
+ metadata?: TMetadata;
+}
+/**
+ * The result of a card completion auto check.
+ * - `true` if the card is complete.
+ * - `false` if the card is not complete.
+ * - `{ isComplete: true, completeBadgeText: ReactNode }` if the card is complete and has a custom complete badge text.
+ * - `{ isComplete: false, additionalBadges: ReactNode[] }` if the card is complete and has to show additional badges on the header.
+ * - `{ isComplete: false, metadata: {showWarningCallOut: true} }` if the card is not complete and passes some metadata to the card component.
+ */
+export type CheckCompleteResponse =
+ | boolean
+ | ({ isComplete: boolean } & CheckCompleteResult);
+
+export type SetComplete = (isComplete: boolean) => void;
+export type IsCardComplete = (cardId: OnboardingCardId) => boolean;
+export type SetExpandedCardId = (
+ cardId: OnboardingCardId | null,
+ options?: { scroll?: boolean }
+) => void;
+
+export type OnboardingCardComponent = React.ComponentType<{
+ /**
+ * Function to set the current card completion status.
+ */
+ setComplete: SetComplete;
+ /**
+ * Function to check the current card completion status again.
+ */
+ checkComplete: () => void;
+ /**
+ * Function to check if a specific card is complete.
+ */
+ isCardComplete: IsCardComplete;
+ /**
+ * Function to expand a specific card ID and scroll to it.
+ */
+ setExpandedCardId: SetExpandedCardId;
+ /**
+ * Metadata passed from the card checkComplete function.
+ * It will be `undefined` until the first checkComplete call finishes.
+ */
+ checkCompleteMetadata?: TMetadata;
+}>;
+
+export type OnboardingCardCheckComplete = (
+ services: StartServices
+) => Promise>;
+
+export interface OnboardingCardConfig {
+ id: OnboardingCardId;
+ title: string;
+ icon: IconType;
+ /**
+ * Component that renders the card content when expanded.
+ * It receives a `setComplete` function to allow the card to mark itself as complete if needed.
+ * Please use React.lazy() to load the component.
+ */
+ Component: React.LazyExoticComponent>;
+ /**
+ * Function for auto-checking completion for the card
+ * @returns Promise for the complete status
+ */
+ checkComplete?: OnboardingCardCheckComplete;
+ /**
+ * The RBAC capability strings required to enable the card. It uses object dot notation. e.g. `'siem.crud'`.
+ *
+ * The format of the capabilities property supports OR and AND mechanism:
+ *
+ * To specify capabilities in an OR fashion, they can be defined in a single level array like: `capabilities: [cap1, cap2]`.
+ * If either of "cap1 || cap2" is granted the card will be included.
+ *
+ * To specify capabilities with AND conditional, use a second level array: `capabilities: [['cap1', 'cap2']]`.
+ * This would result in the boolean expression "cap1 && cap2", both capabilities must be granted to include the card.
+ *
+ * They can also be combined like: `capabilities: ['cap1', ['cap2', 'cap3']]` which would result in the boolean expression "cap1 || (cap2 && cap3)".
+ *
+ * For the single capability requirement: `capabilities: 'cap1'`, which is the same as `capabilities: ['cap1']`
+ *
+ * Default is `undefined` (no capabilities required)
+ */
+ capabilities?: RequiredCapabilities;
+ /**
+ * Minimum license required to enable the card.
+ * Default is `basic`
+ */
+ licenseType?: LicenseType;
+}
+
+export interface OnboardingGroupConfig {
+ title: string;
+ // It's not possible to type the cards array with the generic type for all the cards metadata
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ cards: Array>;
+}
diff --git a/x-pack/plugins/security_solution/public/overview/links.ts b/x-pack/plugins/security_solution/public/overview/links.ts
index 0f3bdac0786ec..ab2f64bf1b98a 100644
--- a/x-pack/plugins/security_solution/public/overview/links.ts
+++ b/x-pack/plugins/security_solution/public/overview/links.ts
@@ -10,18 +10,11 @@ import {
DATA_QUALITY_PATH,
DETECTION_RESPONSE_PATH,
ENTITY_ANALYTICS_PATH,
- LANDING_PATH,
OVERVIEW_PATH,
SecurityPageName,
SERVER_APP_ID,
} from '../../common/constants';
-import {
- DATA_QUALITY,
- DETECTION_RESPONSE,
- GETTING_STARTED,
- OVERVIEW,
- ENTITY_ANALYTICS,
-} from '../app/translations';
+import { DATA_QUALITY, DETECTION_RESPONSE, OVERVIEW, ENTITY_ANALYTICS } from '../app/translations';
import type { LinkItem } from '../common/links/types';
import overviewPageImg from '../common/images/overview_page.png';
import dataQualityDashboardPageImg from '../common/images/data_quality_dashboard_page.png';
@@ -45,22 +38,6 @@ export const overviewLinks: LinkItem = {
],
};
-export const gettingStartedLinks: LinkItem = {
- id: SecurityPageName.landing,
- title: GETTING_STARTED,
- path: LANDING_PATH,
- capabilities: [`${SERVER_APP_ID}.show`],
- globalSearchKeywords: [
- i18n.translate('xpack.securitySolution.appLinks.getStarted', {
- defaultMessage: 'Getting started',
- }),
- ],
- sideNavIcon: 'launch',
- sideNavFooter: true,
- skipUrlState: true,
- hideTimeline: true,
-};
-
export const detectionResponseLinks: LinkItem = {
id: SecurityPageName.detectionAndResponse,
title: DETECTION_RESPONSE,
diff --git a/x-pack/plugins/security_solution/public/overview/pages/landing.test.tsx b/x-pack/plugins/security_solution/public/overview/pages/landing.test.tsx
deleted file mode 100644
index e40a2b383196a..0000000000000
--- a/x-pack/plugins/security_solution/public/overview/pages/landing.test.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import { render } from '@testing-library/react';
-import { LandingPage } from './landing';
-import { Router } from '@kbn/shared-ux-router';
-import { createBrowserHistory } from 'history';
-import type { PropsWithChildren } from 'react';
-import React from 'react';
-
-jest.mock('../../common/components/landing_page');
-jest.mock('../../common/components/page_wrapper', () => ({
- SecuritySolutionPageWrapper: jest
- .fn()
- .mockImplementation(({ children }: PropsWithChildren) => {children}
),
-}));
-const history = createBrowserHistory();
-describe('LandingPage', () => {
- it('renders page properly', () => {
- const { queryByTestId } = render(
-
-
-
- );
- expect(queryByTestId('siem-landing-page')).toBeInTheDocument();
- });
-});
diff --git a/x-pack/plugins/security_solution/public/overview/pages/landing.tsx b/x-pack/plugins/security_solution/public/overview/pages/landing.tsx
deleted file mode 100644
index 5ce2bc36afb2e..0000000000000
--- a/x-pack/plugins/security_solution/public/overview/pages/landing.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import React, { memo } from 'react';
-import { css } from '@emotion/react';
-import { SpyRoute } from '../../common/utils/route/spy_routes';
-import { SecurityPageName } from '../../../common/constants';
-import { LandingPageComponent } from '../../common/components/landing_page';
-import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper';
-
-export const LandingPage = memo(() => {
- return (
-
-
-
-
-
-
- );
-});
-
-LandingPage.displayName = 'LandingPage';
diff --git a/x-pack/plugins/security_solution/public/overview/routes.tsx b/x-pack/plugins/security_solution/public/overview/routes.tsx
index 82214b2463bdf..3b1e9ca0e8046 100644
--- a/x-pack/plugins/security_solution/public/overview/routes.tsx
+++ b/x-pack/plugins/security_solution/public/overview/routes.tsx
@@ -8,7 +8,6 @@
import React from 'react';
import { TrackApplicationView } from '@kbn/usage-collection-plugin/public';
import {
- LANDING_PATH,
OVERVIEW_PATH,
DATA_QUALITY_PATH,
DETECTION_RESPONSE_PATH,
@@ -23,7 +22,6 @@ import { DetectionResponse } from './pages/detection_response';
import { PluginTemplateWrapper } from '../common/components/plugin_template_wrapper';
import { EntityAnalyticsPage } from '../entity_analytics/pages/entity_analytics_dashboard';
import { SecurityRoutePageWrapper } from '../common/components/security_route_page_wrapper';
-import { LandingPage } from './pages/landing';
const OverviewRoutes = () => (
@@ -41,14 +39,6 @@ const DetectionResponseRoutes = () => (
);
-const LandingRoutes = () => (
-
-
-
-
-
-);
-
const EntityAnalyticsRoutes = () => (
@@ -74,10 +64,6 @@ export const routes: SecuritySubPluginRoutes = [
path: DETECTION_RESPONSE_PATH,
component: DetectionResponseRoutes,
},
- {
- path: LANDING_PATH,
- render: LandingRoutes,
- },
{
path: ENTITY_ANALYTICS_PATH,
render: EntityAnalyticsRoutes,
diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx
index 9833414b7a31f..17f1ba842f8cb 100644
--- a/x-pack/plugins/security_solution/public/plugin.tsx
+++ b/x-pack/plugins/security_solution/public/plugin.tsx
@@ -234,6 +234,7 @@ export class Plugin implements IPlugin;
constructor(private readonly experimentalFeatures: ExperimentalFeatures) {
- this.onboardingPageService = new OnboardingPageService();
+ this.onboardingService = new OnboardingService();
this.componentsService = new ContractComponentsService();
this.upsellingService = new UpsellingService();
this.isSolutionNavigationEnabled$ = new BehaviorSubject(false); // defaults to classic navigation
@@ -37,7 +37,6 @@ export class PluginContract {
public getStartContract(core: CoreStart): PluginStart {
return {
- setOnboardingPageSettings: this.onboardingPageService,
getNavLinks$: () => navLinks$,
setComponents: (components) => {
this.componentsService.setComponents(components);
@@ -49,6 +48,7 @@ export class PluginContract {
this.isSolutionNavigationEnabled$.next(isSolutionNavigationEnabled);
updateNavLinks(isSolutionNavigationEnabled, core);
},
+ setOnboardingSettings: this.onboardingService.setSettings.bind(this.onboardingService),
getSolutionNavigation: () => lazySolutionNavigation(core),
};
}
@@ -57,7 +57,7 @@ export class PluginContract {
return {
getComponents$: this.componentsService.getComponents$.bind(this.componentsService),
upselling: this.upsellingService,
- onboarding: this.onboardingPageService,
+ onboarding: this.onboardingService,
};
}
}
diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts
index ca144c21d7847..b9dd41a80e668 100644
--- a/x-pack/plugins/security_solution/public/types.ts
+++ b/x-pack/plugins/security_solution/public/types.ts
@@ -66,6 +66,7 @@ import type { Detections } from './detections';
import type { Cases } from './cases';
import type { Exceptions } from './exceptions';
import type { Kubernetes } from './kubernetes';
+import type { Onboarding } from './onboarding';
import type { Overview } from './overview';
import type { Rules } from './rules';
import type { Timelines } from './timelines';
@@ -89,7 +90,7 @@ import type { TopValuesPopoverService } from './app/components/top_values_popove
import type { ExperimentalFeatures } from '../common/experimental_features';
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 { OnboardingService } from './onboarding/service';
import type { SolutionNavigation } from './app/solution_navigation/solution_navigation';
export interface SetupPlugins {
@@ -163,7 +164,7 @@ export interface StartPluginsDependencies extends StartPlugins {
export interface ContractStartServices {
getComponents$: GetComponents$;
upselling: UpsellingService;
- onboarding: OnboardingPageService;
+ onboarding: OnboardingService;
}
export type StartServices = CoreStart &
@@ -211,7 +212,7 @@ export interface PluginStart {
setComponents: SetComponents;
getBreadcrumbsNav$: () => Observable;
getUpselling: () => UpsellingService;
- setOnboardingPageSettings: OnboardingPageService;
+ setOnboardingSettings: OnboardingService['setSettings'];
setIsSolutionNavigationEnabled: (isSolutionNavigationEnabled: boolean) => void;
getSolutionNavigation: () => Promise;
}
@@ -231,6 +232,7 @@ export interface SubPlugins {
explore: Explore;
kubernetes: Kubernetes;
management: Management;
+ onboarding: Onboarding;
overview: Overview;
rules: Rules;
threatIntelligence: ThreatIntelligence;
@@ -253,6 +255,7 @@ export interface StartedSubPlugins {
explore: ReturnType;
kubernetes: ReturnType;
management: ReturnType;
+ onboarding: ReturnType;
overview: ReturnType;
rules: ReturnType;
threatIntelligence: ReturnType;
diff --git a/x-pack/plugins/security_solution_ess/public/onboarding/onboarding.ts b/x-pack/plugins/security_solution_ess/public/onboarding/onboarding.ts
index a5e32e25644bb..62c84c5234fe1 100644
--- a/x-pack/plugins/security_solution_ess/public/onboarding/onboarding.ts
+++ b/x-pack/plugins/security_solution_ess/public/onboarding/onboarding.ts
@@ -5,30 +5,13 @@
* 2.0.
*/
-import {
- AddIntegrationsSteps,
- EnablePrebuiltRulesSteps,
- OverviewSteps,
- ViewAlertsSteps,
- ViewDashboardSteps,
-} from '@kbn/security-solution-plugin/public';
import type { Services } from '../common/services';
export const setOnboardingSettings = (services: Services) => {
- const {
- securitySolution,
- application: { getUrlForApp },
- } = services;
+ const { securitySolution, application } = services;
- securitySolution.setOnboardingPageSettings.setUsersUrl(
- getUrlForApp('management', { path: 'security/users' })
- );
-
- securitySolution.setOnboardingPageSettings.setAvailableSteps([
- OverviewSteps.getToKnowElasticSecurity,
- AddIntegrationsSteps.connectToDataSources,
- ViewDashboardSteps.analyzeData,
- EnablePrebuiltRulesSteps.enablePrebuiltRules,
- ViewAlertsSteps.viewAlerts,
- ]);
+ securitySolution.setOnboardingSettings({
+ userUrl: application.getUrlForApp('management', { path: 'security/users/create' }),
+ isAgentlessAvailable: false,
+ });
};
diff --git a/x-pack/plugins/security_solution_serverless/public/onboarding/onboarding.ts b/x-pack/plugins/security_solution_serverless/public/onboarding/onboarding.ts
index cf89e156e4f5a..e371ccfc42c2a 100644
--- a/x-pack/plugins/security_solution_serverless/public/onboarding/onboarding.ts
+++ b/x-pack/plugins/security_solution_serverless/public/onboarding/onboarding.ts
@@ -5,25 +5,14 @@
* 2.0.
*/
-import {
- AddIntegrationsSteps,
- CreateProjectSteps,
- EnablePrebuiltRulesSteps,
- OverviewSteps,
- ViewAlertsSteps,
- ViewDashboardSteps,
-} from '@kbn/security-solution-plugin/public';
+import { getCloudUrl } from '../navigation/util';
import type { Services } from '../common/services';
export const setOnboardingSettings = (services: Services) => {
- const { securitySolution } = services;
+ const { securitySolution, cloud } = services;
- securitySolution.setOnboardingPageSettings.setAvailableSteps([
- CreateProjectSteps.createFirstProject,
- OverviewSteps.getToKnowElasticSecurity,
- AddIntegrationsSteps.connectToDataSources,
- ViewDashboardSteps.analyzeData,
- EnablePrebuiltRulesSteps.enablePrebuiltRules,
- ViewAlertsSteps.viewAlerts,
- ]);
+ securitySolution.setOnboardingSettings({
+ userUrl: getCloudUrl('usersAndRoles', cloud),
+ isAgentlessAvailable: true,
+ });
};
diff --git a/x-pack/plugins/security_solution_serverless/public/plugin.ts b/x-pack/plugins/security_solution_serverless/public/plugin.ts
index 9bc7df9972b5c..4d94a11500d50 100644
--- a/x-pack/plugins/security_solution_serverless/public/plugin.ts
+++ b/x-pack/plugins/security_solution_serverless/public/plugin.ts
@@ -22,7 +22,6 @@ import {
parseExperimentalConfigValue,
type ExperimentalFeatures,
} from '../common/experimental_features';
-import { getCloudUrl, getProjectFeaturesUrl } from './navigation/util';
import { setOnboardingSettings } from './onboarding';
export class SecuritySolutionServerlessPlugin
@@ -71,16 +70,7 @@ export class SecuritySolutionServerlessPlugin
securitySolution.setComponents({
DashboardsLandingCallout: getDashboardsLandingCallout(services),
});
- securitySolution.setOnboardingPageSettings.setProductTypes(productTypes);
- securitySolution.setOnboardingPageSettings.setProjectFeaturesUrl(
- getProjectFeaturesUrl(services.cloud)
- );
- securitySolution.setOnboardingPageSettings.setProjectsUrl(
- getCloudUrl('projects', services.cloud)
- );
- securitySolution.setOnboardingPageSettings.setUsersUrl(
- getCloudUrl('usersAndRoles', services.cloud)
- );
+
setOnboardingSettings(services);
startNavigation(services);
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index 26ef42752ae79..c4b852ac5ec0f 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -39609,10 +39609,6 @@
"xpack.securitySolution.notes.search.FilterByUserOrNotePlaceholder": "Filtre par utilisateur ou note",
"xpack.securitySolution.notes.youAreEditingANoteScreenReaderOnly": "Vous modifiez une note. Cliquez sur Ajouter la note lorsque vous aurez terminé, ou appuyez sur Échap pour annuler.",
"xpack.securitySolution.notes.youAreViewingNotesScreenReaderOnly": "Vous visualisez des notes pour l'événement de la ligne {row}. Appuyez sur la touche fléchée vers le haut lorsque vous aurez terminé pour revenir à l'événement.",
- "xpack.securitySolution.onboarding.addIntegrationCallout.description": "Pour {stepName} ajouter d'abord des intégrations {addIntegration}",
- "xpack.securitySolution.onboarding.addIntegrationCallout.link.action": "autoriser cette étape",
- "xpack.securitySolution.onboarding.currentPlan.label": "Planification actuelle :",
- "xpack.securitySolution.onboarding.currentTier.label": "Niveau actuel :",
"xpack.securitySolution.onboarding.description": "Cette aire montre tout ce que vous devez savoir. Vous pouvez explorer tout le contenu. Vous pouvez revenir ici à tout moment.",
"xpack.securitySolution.onboarding.footer.demo.description": "Découvrir Elastic en utilisant des exemples de données",
"xpack.securitySolution.onboarding.footer.demo.link.title": "Explorer la démonstration",
@@ -39626,58 +39622,8 @@
"xpack.securitySolution.onboarding.footer.labs.description": "Points de vue des chercheurs en sécurité",
"xpack.securitySolution.onboarding.footer.labs.link.title": "En savoir plus",
"xpack.securitySolution.onboarding.footer.labs.title": "Ateliers Elastic Security",
- "xpack.securitySolution.onboarding.progressTracker.progressBar.label": "PROGRESSION",
- "xpack.securitySolution.onboarding.step.addIntegrations.addAllIntegrations.description": "L'ensemble des intégrations de sécurité",
- "xpack.securitySolution.onboarding.step.addIntegrations.addAllIntegrations.title": "Toutes les intégrations de sécurité",
- "xpack.securitySolution.onboarding.step.addIntegrations.addCloudIntegrations.description": "Intégrations de sécurité spécifiques au cloud",
- "xpack.securitySolution.onboarding.step.addIntegrations.addCloudIntegrations.title": "Ajouter des données sur la sécurité du cloud",
- "xpack.securitySolution.onboarding.step.addIntegrations.addEdrXdrIntegrations.description": "Intégrations de sécurité spécifiques à l'EDR / XDR",
- "xpack.securitySolution.onboarding.step.addIntegrations.addEdrXdrIntegrations.title": "Ajouter des données EDR / XDR",
- "xpack.securitySolution.onboarding.step.addIntegrations.description": "Utilisez des intégrations pour importer les données depuis des sources communes et pour aider à rassembler les informations pertinentes au même endroit.",
- "xpack.securitySolution.onboarding.step.addIntegrations.image.title": "Se connecter aux sources de données existantes",
- "xpack.securitySolution.onboarding.step.addIntegrations.seeIntegrationsButton": "Afficher les intégrations",
- "xpack.securitySolution.onboarding.step.addIntegrations.title": "Ajouter des données avec des intégrations",
- "xpack.securitySolution.onboarding.step.createProject.description": "Créer un projet Elastic Security avec nos solutions sans serveur entièrement gérées qui gèrent automatiquement les nœuds, les partitions, les niveaux de données et la montée en charge pour assurer la maintenance de la santé et de la performance pour vous permettre de vous concentrer sur les données et les objectifs.",
- "xpack.securitySolution.onboarding.step.createProject.title": "Créer votre premier projet",
- "xpack.securitySolution.onboarding.step.enableRules.description": "Elastic Security est fourni avec des règles de détection prédéfinies qui s'exécutent en arrière-plan et créent des alertes lorsque leurs conditions sont remplies.",
- "xpack.securitySolution.onboarding.step.enableRules.title": "Activer les règles prédéfinies",
- "xpack.securitySolution.onboarding.step.viewAlerts.description": "Visualisez, triez, filtrez et examinez les alertes dans votre infrastructure. Examinez des alertes individuelles dignes d'intérêt et découvrez des tendances générales dans le volume et la gravité des alertes.",
- "xpack.securitySolution.onboarding.step.viewAlerts.title": "Afficher les alertes",
- "xpack.securitySolution.onboarding.step.viewDashboards.description": "Utilisez des tableaux de bord pour visualiser les données et être au courant des dernières informations essentielles. Créez votre propre tableau de bord ou utilisez celui proposé par défaut par Elastic, en ajoutant des alertes, des événements d'authentification des utilisateurs, les vulnérabilités connues et plus encore.",
- "xpack.securitySolution.onboarding.step.viewDashboards.image.title": "Analysez des données à l'aide de tableaux de bord",
- "xpack.securitySolution.onboarding.step.viewDashboards.title": "Voir et analyser vos donner grâce aux tableaux de bord",
- "xpack.securitySolution.onboarding.step.watchVideo.button.title": "Elastic Security",
- "xpack.securitySolution.onboarding.step.watchVideo.description1": "Elastic Security est une solution SaaS qui regroupe des capacités analytiques, EDR, de sécurité du cloud et autres, avec pour vocation de soutenir l'amélioration du niveau de sécurité de votre organisation, de la défendre contre un vaste éventail de menaces et de prévenir toutes failles éventuelles.\n ",
- "xpack.securitySolution.onboarding.step.watchVideo.description2": "Pour découvrir les fonctionnalités qui se trouvent au cœur de la plateforme, regardez cette vidéo :",
- "xpack.securitySolution.onboarding.step.watchVideo.title": "Regardez la vidéo de présentation",
"xpack.securitySolution.onboarding.subTitle": "Démarrer avec Security",
- "xpack.securitySolution.onboarding.task.addElasticRules": "Ajouter des règles Elastic",
- "xpack.securitySolution.onboarding.task.addElasticRules.callout.title": "ajouter des règles Elastic",
- "xpack.securitySolution.onboarding.task.addIntegrationsStep.title": "Ajouter des étapes d'intégration",
- "xpack.securitySolution.onboarding.task.goToDashboards": "Accéder aux tableaux de bord",
- "xpack.securitySolution.onboarding.task.manageProjects": "Gérer les projets",
- "xpack.securitySolution.onboarding.task.viewAlerts": "Afficher les alertes",
- "xpack.securitySolution.onboarding.task.viewAlerts.callout.title": "afficher les alertes",
- "xpack.securitySolution.onboarding.task.viewDashboards.callout.title": "afficher les tableaux de bord",
"xpack.securitySolution.onboarding.Title": "Bonjour, {userName} !",
- "xpack.securitySolution.onboarding.togglePanel.done.title": "Étape terminée",
- "xpack.securitySolution.onboarding.togglePanel.empty.description": "Activez une bascule pour poursuivre votre expérience \"Démarrer\" organisée",
- "xpack.securitySolution.onboarding.togglePanel.empty.title": "Hmm, on dirait qu’il n’y a rien ici",
- "xpack.securitySolution.onboarding.togglePanel.expandStepButton.ariaLabel": "Développer \"{title}\"",
- "xpack.securitySolution.onboarding.togglePanel.markAsDoneTitle": "Marquer comme terminé",
- "xpack.securitySolution.onboarding.togglePanel.productBadge.analytics": "Analyse",
- "xpack.securitySolution.onboarding.togglePanel.productBadge.cloud": "Cloud",
- "xpack.securitySolution.onboarding.togglePanel.productBadge.edr": "EDR",
- "xpack.securitySolution.onboarding.togglePanel.section1.title": "Démarrage rapide",
- "xpack.securitySolution.onboarding.togglePanel.section2.title": "Ajouter et valider vos données",
- "xpack.securitySolution.onboarding.togglePanel.section3.title": "Démarrer avec les alertes",
- "xpack.securitySolution.onboarding.togglePanel.switch.analytics.label": "Analyse",
- "xpack.securitySolution.onboarding.togglePanel.switch.cloud.label": "Sécurité du cloud",
- "xpack.securitySolution.onboarding.togglePanel.switch.endpoint.label": "Endpoint Security",
- "xpack.securitySolution.onboarding.togglePanel.undoMarkAsDoneTitle": "Annuler \"Marquer comme terminé\"",
- "xpack.securitySolution.onboarding.welcomePanel.productTier.complete": "Terminé",
- "xpack.securitySolution.onboarding.welcomePanel.productTier.essential": "Essentiel",
- "xpack.securitySolution.onboardingProductLabel.title": "Élaborez votre expérience Démarrer :",
"xpack.securitySolution.open.timeline.batchActionsTitle": "Actions groupées",
"xpack.securitySolution.open.timeline.cancelButton": "Annuler",
"xpack.securitySolution.open.timeline.collapseButton": "Réduire",
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index b73b085089dc3..43dc1c697b9b7 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -39353,10 +39353,6 @@
"xpack.securitySolution.notes.search.FilterByUserOrNotePlaceholder": "ユーザーまたはメモでフィルター",
"xpack.securitySolution.notes.youAreEditingANoteScreenReaderOnly": "メモを編集しています。完了したら[メモの追加]をクリックするか、Esc を押してキャンセルします。",
"xpack.securitySolution.notes.youAreViewingNotesScreenReaderOnly": "行 {row} のイベントのメモを表示しています。完了したら上矢印キーを押して、イベントに戻ります。",
- "xpack.securitySolution.onboarding.addIntegrationCallout.description": "{stepName}には、最初の{addIntegration}を追加します",
- "xpack.securitySolution.onboarding.addIntegrationCallout.link.action": "このステップを有効化",
- "xpack.securitySolution.onboarding.currentPlan.label": "現在の計画:",
- "xpack.securitySolution.onboarding.currentTier.label": "現在のティア:",
"xpack.securitySolution.onboarding.description": "このエリアには、必要な情報がすべて表示されます。すべてのコンテンツを自由にご覧ください。いつでも戻ってくることができます。",
"xpack.securitySolution.onboarding.footer.demo.description": "サンプルデータを使用してElasticを探索",
"xpack.securitySolution.onboarding.footer.demo.link.title": "デモの探索",
@@ -39370,58 +39366,8 @@
"xpack.securitySolution.onboarding.footer.labs.description": "セキュリティ研究者によるインサイト",
"xpack.securitySolution.onboarding.footer.labs.link.title": "詳細",
"xpack.securitySolution.onboarding.footer.labs.title": "Elastic Security Labs",
- "xpack.securitySolution.onboarding.progressTracker.progressBar.label": "進捗",
- "xpack.securitySolution.onboarding.step.addIntegrations.addAllIntegrations.description": "すべてが揃ったセキュリティ統合",
- "xpack.securitySolution.onboarding.step.addIntegrations.addAllIntegrations.title": "すべてのセキュリティ統合",
- "xpack.securitySolution.onboarding.step.addIntegrations.addCloudIntegrations.description": "クラウド固有のセキュリティ統合",
- "xpack.securitySolution.onboarding.step.addIntegrations.addCloudIntegrations.title": "クラウドセキュリティデータを追加",
- "xpack.securitySolution.onboarding.step.addIntegrations.addEdrXdrIntegrations.description": "EDR/XDR固有のセキュリティ統合",
- "xpack.securitySolution.onboarding.step.addIntegrations.addEdrXdrIntegrations.title": "EDR/XDRデータを追加",
- "xpack.securitySolution.onboarding.step.addIntegrations.description": "統合機能を使用して、一般的なソースからデータをインポートし、関連情報を1つの場所に集めることができます。",
- "xpack.securitySolution.onboarding.step.addIntegrations.image.title": "既存のデータソースに接続",
- "xpack.securitySolution.onboarding.step.addIntegrations.seeIntegrationsButton": "統合を表示",
- "xpack.securitySolution.onboarding.step.addIntegrations.title": "統合によってデータを追加",
- "xpack.securitySolution.onboarding.step.createProject.description": "ノード、シャード、データティア、スケーリングを自動的に管理し、正常性とパフォーマンスを維持するフルマネージドサーバーレスソリューションでElastic Securityプロジェクトを構築することで、お客様はデータや目標に集中することができます。",
- "xpack.securitySolution.onboarding.step.createProject.title": "最初のプロジェクトを作成",
- "xpack.securitySolution.onboarding.step.enableRules.description": "Elasticセキュリティには、バックグラウンドで実行され、条件が合うとアラートを作成する事前構築済み検出ルールがあります。",
- "xpack.securitySolution.onboarding.step.enableRules.title": "事前構築済みのルールを有効化",
- "xpack.securitySolution.onboarding.step.viewAlerts.description": "インフラ全体からアラートを可視化、並べ替え、フィルタリング、調査します。関心のある個々のアラートを調査し、アラート量と重大度の一般的なパターンを発見します。",
- "xpack.securitySolution.onboarding.step.viewAlerts.title": "アラートを表示",
- "xpack.securitySolution.onboarding.step.viewDashboards.description": "ダッシュボードを使用してデータを可視化し、重要な情報を常に最新の状態に保ちます。アラート、ユーザー認証イベント、既知の脆弱性など、独自のダッシュボードを作成したり、Elasticのデフォルトのダッシュボードを使用したりできます。",
- "xpack.securitySolution.onboarding.step.viewDashboards.image.title": "ダッシュボードを使用してデータを分析",
- "xpack.securitySolution.onboarding.step.viewDashboards.title": "ダッシュボードを使用してデータを表示および分析",
- "xpack.securitySolution.onboarding.step.watchVideo.button.title": "Elasticセキュリティ",
- "xpack.securitySolution.onboarding.step.watchVideo.description1": "Elastic Securityは、分析、EDR、クラウドセキュリティ機能などをSaaS型ソリューションに統合し、組織のセキュリティ態勢の改善、幅広い脅威からの防御、侵害の防止を支援します。\n ",
- "xpack.securitySolution.onboarding.step.watchVideo.description2": "このプラットフォームの主な機能については、動画をご覧ください。",
- "xpack.securitySolution.onboarding.step.watchVideo.title": "概要動画を視聴",
"xpack.securitySolution.onboarding.subTitle": "Securityの基本",
- "xpack.securitySolution.onboarding.task.addElasticRules": "Elasticルールを追加",
- "xpack.securitySolution.onboarding.task.addElasticRules.callout.title": "Elasticルールを追加",
- "xpack.securitySolution.onboarding.task.addIntegrationsStep.title": "統合ステップの追加",
- "xpack.securitySolution.onboarding.task.goToDashboards": "ダッシュボードに移動",
- "xpack.securitySolution.onboarding.task.manageProjects": "プロジェクトの管理",
- "xpack.securitySolution.onboarding.task.viewAlerts": "アラートを表示",
- "xpack.securitySolution.onboarding.task.viewAlerts.callout.title": "アラートを表示",
- "xpack.securitySolution.onboarding.task.viewDashboards.callout.title": "ダッシュボードを表示",
"xpack.securitySolution.onboarding.Title": "こんにちは、{userName}さん!",
- "xpack.securitySolution.onboarding.togglePanel.done.title": "ステップ完了",
- "xpack.securitySolution.onboarding.togglePanel.empty.description": "トグルをオンにすると、キュレーションされた「基本操作」エクスペリエンスを続行できます",
- "xpack.securitySolution.onboarding.togglePanel.empty.title": "何もないようです",
- "xpack.securitySolution.onboarding.togglePanel.expandStepButton.ariaLabel": "\"{title}\"を展開",
- "xpack.securitySolution.onboarding.togglePanel.markAsDoneTitle": "完了に設定",
- "xpack.securitySolution.onboarding.togglePanel.productBadge.analytics": "分析",
- "xpack.securitySolution.onboarding.togglePanel.productBadge.cloud": "クラウド",
- "xpack.securitySolution.onboarding.togglePanel.productBadge.edr": "EDR",
- "xpack.securitySolution.onboarding.togglePanel.section1.title": "クイックスタート",
- "xpack.securitySolution.onboarding.togglePanel.section2.title": "データを追加して検証",
- "xpack.securitySolution.onboarding.togglePanel.section3.title": "アラートの基本",
- "xpack.securitySolution.onboarding.togglePanel.switch.analytics.label": "分析",
- "xpack.securitySolution.onboarding.togglePanel.switch.cloud.label": "クラウドセキュリティ",
- "xpack.securitySolution.onboarding.togglePanel.switch.endpoint.label": "Endpoint Security",
- "xpack.securitySolution.onboarding.togglePanel.undoMarkAsDoneTitle": "[完了に設定]を元に戻す",
- "xpack.securitySolution.onboarding.welcomePanel.productTier.complete": "完了",
- "xpack.securitySolution.onboarding.welcomePanel.productTier.essential": "基本",
- "xpack.securitySolution.onboardingProductLabel.title": "基本操作エクスペリエンスをキュレーション:",
"xpack.securitySolution.open.timeline.batchActionsTitle": "一斉アクション",
"xpack.securitySolution.open.timeline.cancelButton": "キャンセル",
"xpack.securitySolution.open.timeline.collapseButton": "縮小",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index ab2b923985a1b..9aeb68dff82e5 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -39398,10 +39398,6 @@
"xpack.securitySolution.notes.search.FilterByUserOrNotePlaceholder": "按用户或备注筛选",
"xpack.securitySolution.notes.youAreEditingANoteScreenReaderOnly": "您正在编辑备注。完成后单击“添加备注”,或按 esc 键取消。",
"xpack.securitySolution.notes.youAreViewingNotesScreenReaderOnly": "您正在查看事件在第 {row} 行的备注。完成后,按向上箭头键可返回到事件。",
- "xpack.securitySolution.onboarding.addIntegrationCallout.description": "要 {stepName},请先添加集成 {addIntegration}",
- "xpack.securitySolution.onboarding.addIntegrationCallout.link.action": "启用此步骤",
- "xpack.securitySolution.onboarding.currentPlan.label": "当前计划:",
- "xpack.securitySolution.onboarding.currentTier.label": "当前层:",
"xpack.securitySolution.onboarding.description": "此区域向您显示您需要了解的一切内容。请随时浏览所有内容。您始终可以随时返回此处。",
"xpack.securitySolution.onboarding.footer.demo.description": "使用样例数据发现 Elastic",
"xpack.securitySolution.onboarding.footer.demo.link.title": "浏览演示",
@@ -39415,58 +39411,8 @@
"xpack.securitySolution.onboarding.footer.labs.description": "安全研究员的见解",
"xpack.securitySolution.onboarding.footer.labs.link.title": "了解详情",
"xpack.securitySolution.onboarding.footer.labs.title": "Elastic Security Labs",
- "xpack.securitySolution.onboarding.progressTracker.progressBar.label": "进度",
- "xpack.securitySolution.onboarding.step.addIntegrations.addAllIntegrations.description": "一整组安全集成",
- "xpack.securitySolution.onboarding.step.addIntegrations.addAllIntegrations.title": "所有安全集成",
- "xpack.securitySolution.onboarding.step.addIntegrations.addCloudIntegrations.description": "特定于云的安全集成",
- "xpack.securitySolution.onboarding.step.addIntegrations.addCloudIntegrations.title": "添加云安全数据",
- "xpack.securitySolution.onboarding.step.addIntegrations.addEdrXdrIntegrations.description": "特定于 EDR/XDR 的安全集成",
- "xpack.securitySolution.onboarding.step.addIntegrations.addEdrXdrIntegrations.title": "添加 EDR/XDR 数据",
- "xpack.securitySolution.onboarding.step.addIntegrations.description": "使用集成从公用源导入数据,并帮助您在一个位置收集相关信息。",
- "xpack.securitySolution.onboarding.step.addIntegrations.image.title": "连接到现有数据源",
- "xpack.securitySolution.onboarding.step.addIntegrations.seeIntegrationsButton": "查看集成",
- "xpack.securitySolution.onboarding.step.addIntegrations.title": "通过集成添加数据",
- "xpack.securitySolution.onboarding.step.createProject.description": "使用我们的全托管无服务器解决方案创建 Elastic Security 项目,该解决方案会自动管理节点、分片、数据层和扩展,以维护运行状况和性能,便于您专注于自己的数据和目标。",
- "xpack.securitySolution.onboarding.step.createProject.title": "创建您的首个项目",
- "xpack.securitySolution.onboarding.step.enableRules.description": "Elastic Security 附带预置检测规则,这些规则在后台运行,并在条件得到满足时创建告警。",
- "xpack.securitySolution.onboarding.step.enableRules.title": "启用预构建的规则",
- "xpack.securitySolution.onboarding.step.viewAlerts.description": "对整个基础设施中的告警进行可视化、排序、筛选和调查。检查各个相关告警,并发现告警量和严重性方面的通用模式。",
- "xpack.securitySolution.onboarding.step.viewAlerts.title": "查看告警",
- "xpack.securitySolution.onboarding.step.viewDashboards.description": "使用仪表板进行数据可视化,并及时了解最新关键信息。创建您自己的仪表板,或使用 Elastic 的默认仪表板 — 包括告警、用户身份验证事件、已知漏洞等。",
- "xpack.securitySolution.onboarding.step.viewDashboards.image.title": "使用仪表板分析数据",
- "xpack.securitySolution.onboarding.step.viewDashboards.title": "使用仪表板查看和分析数据",
- "xpack.securitySolution.onboarding.step.watchVideo.button.title": "Elastic Security",
- "xpack.securitySolution.onboarding.step.watchVideo.description1": "Elastic Security 将分析、EDR、云安全功能等集成到一个 SaaS 解决方案中,可帮助您改善组织的安全态势,防范一系列威胁并防止泄漏。\n ",
- "xpack.securitySolution.onboarding.step.watchVideo.description2": "要了解该平台的核心功能,请观看以下视频:",
- "xpack.securitySolution.onboarding.step.watchVideo.title": "观看概述视频",
"xpack.securitySolution.onboarding.subTitle": "Security 入门",
- "xpack.securitySolution.onboarding.task.addElasticRules": "添加 Elastic 规则",
- "xpack.securitySolution.onboarding.task.addElasticRules.callout.title": "添加 Elastic 规则",
- "xpack.securitySolution.onboarding.task.addIntegrationsStep.title": "添加集成步骤",
- "xpack.securitySolution.onboarding.task.goToDashboards": "前往仪表板",
- "xpack.securitySolution.onboarding.task.manageProjects": "管理项目",
- "xpack.securitySolution.onboarding.task.viewAlerts": "查看告警",
- "xpack.securitySolution.onboarding.task.viewAlerts.callout.title": "查看告警",
- "xpack.securitySolution.onboarding.task.viewDashboards.callout.title": "查看仪表板",
"xpack.securitySolution.onboarding.Title": "{userName} 您好!",
- "xpack.securitySolution.onboarding.togglePanel.done.title": "步骤已完成",
- "xpack.securitySolution.onboarding.togglePanel.empty.description": "打开切换按钮,继续您策展的“开始使用”体验",
- "xpack.securitySolution.onboarding.togglePanel.empty.title": "嗯,那里似乎没有任何内容",
- "xpack.securitySolution.onboarding.togglePanel.expandStepButton.ariaLabel": "展开“{title}”",
- "xpack.securitySolution.onboarding.togglePanel.markAsDoneTitle": "标记为已完成",
- "xpack.securitySolution.onboarding.togglePanel.productBadge.analytics": "分析",
- "xpack.securitySolution.onboarding.togglePanel.productBadge.cloud": "云",
- "xpack.securitySolution.onboarding.togglePanel.productBadge.edr": "EDR",
- "xpack.securitySolution.onboarding.togglePanel.section1.title": "快速启动",
- "xpack.securitySolution.onboarding.togglePanel.section2.title": "添加并验证数据",
- "xpack.securitySolution.onboarding.togglePanel.section3.title": "开始使用告警",
- "xpack.securitySolution.onboarding.togglePanel.switch.analytics.label": "分析",
- "xpack.securitySolution.onboarding.togglePanel.switch.cloud.label": "云安全",
- "xpack.securitySolution.onboarding.togglePanel.switch.endpoint.label": "Endpoint Security",
- "xpack.securitySolution.onboarding.togglePanel.undoMarkAsDoneTitle": "撤消“标记为已完成”",
- "xpack.securitySolution.onboarding.welcomePanel.productTier.complete": "已完成",
- "xpack.securitySolution.onboarding.welcomePanel.productTier.essential": "基本",
- "xpack.securitySolution.onboardingProductLabel.title": "策展您的开始使用体验:",
"xpack.securitySolution.open.timeline.batchActionsTitle": "批处理操作",
"xpack.securitySolution.open.timeline.cancelButton": "取消",
"xpack.securitySolution.open.timeline.collapseButton": "折叠",
diff --git a/x-pack/test_serverless/functional/services/svl_sec_navigation.ts b/x-pack/test_serverless/functional/services/svl_sec_navigation.ts
index bde44125d142f..3fa9cb8af4cd0 100644
--- a/x-pack/test_serverless/functional/services/svl_sec_navigation.ts
+++ b/x-pack/test_serverless/functional/services/svl_sec_navigation.ts
@@ -19,7 +19,7 @@ export function SvlSecNavigationServiceProvider({
async navigateToLandingPage() {
await retry.tryForTime(60 * 1000, async () => {
await PageObjects.common.navigateToApp('landingPage');
- await testSubjects.existOrFail('data-ingestion-hub-header', { timeout: 2000 });
+ await testSubjects.existOrFail('onboarding-hub-page', { timeout: 2000 });
});
},
};