diff --git a/x-pack/plugins/security_solution/public/common/components/empty_prompt/__mocks__/index.tsx b/x-pack/plugins/security_solution/public/common/components/empty_prompt/__mocks__/index.tsx
new file mode 100644
index 0000000000000..ae193bda9e3fc
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/empty_prompt/__mocks__/index.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 EmptyPrompt = () =>
;
diff --git a/x-pack/plugins/security_solution/public/common/components/empty_prompt/constants.ts b/x-pack/plugins/security_solution/public/common/components/empty_prompt/constants.ts
new file mode 100644
index 0000000000000..0cf6b35ceab99
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/empty_prompt/constants.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 const VIDEO_SOURCE = '//play.vidyard.com/K6kKDBbP9SpXife9s2tHNP.html?';
diff --git a/x-pack/plugins/security_solution/public/common/components/empty_prompt/empty_prompt.test.tsx b/x-pack/plugins/security_solution/public/common/components/empty_prompt/empty_prompt.test.tsx
new file mode 100644
index 0000000000000..c64d61d2bd226
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/empty_prompt/empty_prompt.test.tsx
@@ -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 React from 'react';
+import { fireEvent, render } from '@testing-library/react';
+import { EmptyPromptComponent } from './empty_prompt';
+import { SecurityPageName } from '../../../../common';
+import { useNavigateTo } from '../../lib/kibana';
+import { AddIntegrationsSteps } from '../landing_page/onboarding/types';
+
+const mockNavigateTo = jest.fn();
+const mockUseNavigateTo = useNavigateTo as jest.Mock;
+
+jest.mock('../../lib/kibana', () => ({
+ useNavigateTo: jest.fn(),
+}));
+
+describe('EmptyPromptComponent component', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockUseNavigateTo.mockImplementation(() => ({ navigateTo: mockNavigateTo }));
+ });
+
+ it('has add data links', () => {
+ const { getAllByText } = render();
+ expect(getAllByText('Add security integrations')).toHaveLength(2);
+ });
+
+ describe.each(['header', 'footer'])('URLs at the %s', (place) => {
+ it('points to the default Add data URL', () => {
+ const { getByTestId } = render();
+ const link = getByTestId(`add-integrations-${place}`);
+ fireEvent.click(link);
+ expect(mockNavigateTo).toBeCalledWith({
+ deepLinkId: SecurityPageName.landing,
+ path: `#${AddIntegrationsSteps.connectToDataSources}`,
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/common/components/empty_prompt/empty_prompt.tsx b/x-pack/plugins/security_solution/public/common/components/empty_prompt/empty_prompt.tsx
new file mode 100644
index 0000000000000..853b064233c3b
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/empty_prompt/empty_prompt.tsx
@@ -0,0 +1,180 @@
+/*
+ * 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, useCallback } from 'react';
+import {
+ EuiButton,
+ EuiCard,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiPageHeader,
+ useEuiTheme,
+ type EuiThemeComputed,
+} from '@elastic/eui';
+import { css } from '@emotion/react';
+import { SecurityPageName } from '../../../../common';
+
+import * as i18n from './translations';
+import endpointSvg from './images/endpoint1.svg';
+import cloudSvg from './images/cloud1.svg';
+import siemSvg from './images/siem1.svg';
+import { useNavigateTo } from '../../lib/kibana';
+import { VIDEO_SOURCE } from './constants';
+import { AddIntegrationsSteps } from '../landing_page/onboarding/types';
+
+const imgUrls = {
+ cloud: cloudSvg,
+ siem: siemSvg,
+ endpoint: endpointSvg,
+};
+
+const headerCardStyles = css`
+ span.euiTitle {
+ font-size: 36px;
+ line-height: 100%;
+ }
+`;
+
+const pageHeaderStyles = css`
+ h1 {
+ font-size: 18px;
+ }
+`;
+
+const getFlexItemStyles = (euiTheme: EuiThemeComputed) => css`
+ background: ${euiTheme.colors.lightestShade};
+ padding: 20px;
+`;
+
+const cardStyles = css`
+ img {
+ margin-top: 20px;
+ max-width: 400px;
+ }
+`;
+
+const footerStyles = css`
+ span.euiTitle {
+ font-size: 36px;
+ line-height: 100%;
+ }
+ max-width: 600px;
+ display: block;
+ margin: 20px auto 0;
+`;
+
+export const EmptyPromptComponent: React.FC = memo(() => {
+ const { euiTheme } = useEuiTheme();
+
+ const { navigateTo } = useNavigateTo();
+
+ const navigateToAddIntegrations = useCallback(() => {
+ navigateTo({
+ deepLinkId: SecurityPageName.landing,
+ path: `#${AddIntegrationsSteps.connectToDataSources}`,
+ });
+ }, [navigateTo]);
+
+ const onClick = useCallback(() => {
+ navigateToAddIntegrations();
+ }, [navigateToAddIntegrations]);
+
+ return (
+
+
+
+
+
+
+ {i18n.SIEM_CTA}
+
+ }
+ css={headerCardStyles}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {i18n.SIEM_CTA}
+
+ }
+ css={footerStyles}
+ />
+
+
+ );
+});
+EmptyPromptComponent.displayName = 'EmptyPromptComponent';
+
+// eslint-disable-next-line import/no-default-export
+export default EmptyPromptComponent;
diff --git a/x-pack/plugins/security_solution/public/common/components/empty_prompt/images/cloud1.svg b/x-pack/plugins/security_solution/public/common/components/empty_prompt/images/cloud1.svg
new file mode 100644
index 0000000000000..2dc50321505ae
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/empty_prompt/images/cloud1.svg
@@ -0,0 +1,85 @@
+
diff --git a/x-pack/plugins/security_solution/public/common/components/empty_prompt/images/endpoint1.svg b/x-pack/plugins/security_solution/public/common/components/empty_prompt/images/endpoint1.svg
new file mode 100644
index 0000000000000..bbbb1cd59182c
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/empty_prompt/images/endpoint1.svg
@@ -0,0 +1,20 @@
+
diff --git a/x-pack/plugins/security_solution/public/common/components/empty_prompt/images/siem1.svg b/x-pack/plugins/security_solution/public/common/components/empty_prompt/images/siem1.svg
new file mode 100644
index 0000000000000..3d1d43bc47ac3
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/empty_prompt/images/siem1.svg
@@ -0,0 +1,20 @@
+
diff --git a/x-pack/plugins/security_solution/public/common/components/empty_prompt/index.tsx b/x-pack/plugins/security_solution/public/common/components/empty_prompt/index.tsx
new file mode 100644
index 0000000000000..20c05e5f51702
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/empty_prompt/index.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 React, { lazy, Suspense } from 'react';
+import { EuiLoadingLogo } from '@elastic/eui';
+
+const EmptyPromptLazy = lazy(() => import('./empty_prompt'));
+
+const centerLogoStyle = { display: 'flex', margin: 'auto' };
+
+export const EmptyPrompt = () => (
+ }>
+
+
+);
diff --git a/x-pack/plugins/security_solution/public/common/components/empty_prompt/translations.tsx b/x-pack/plugins/security_solution/public/common/components/empty_prompt/translations.tsx
new file mode 100644
index 0000000000000..e23b166a8574d
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/empty_prompt/translations.tsx
@@ -0,0 +1,88 @@
+/*
+ * 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 SIEM_HEADER = i18n.translate(
+ 'xpack.securitySolution.getStarted.landingCards.box.siem.header',
+ {
+ defaultMessage: 'Elastic Security',
+ }
+);
+
+export const SIEM_TITLE = i18n.translate(
+ 'xpack.securitySolution.getStarted.landingCards.box.siem.title',
+ {
+ defaultMessage: 'Security at the speed of Elastic',
+ }
+);
+export const SIEM_DESCRIPTION = i18n.translate(
+ 'xpack.securitySolution.getStarted.landingCards.box.siem.desc',
+ {
+ defaultMessage:
+ 'Elastic Security equips teams to prevent, detect, and respond to threats at cloud speed and scale — securing business operations with a unified, open platform.',
+ }
+);
+export const SIEM_CTA = i18n.translate(
+ 'xpack.securitySolution.getStarted.landingCards.box.siem.cta',
+ {
+ defaultMessage: 'Add security integrations',
+ }
+);
+
+export const ENDPOINT_TITLE = i18n.translate(
+ 'xpack.securitySolution.getStarted.landingCards.box.endpoint.title',
+ {
+ defaultMessage: 'Endpoint security at scale',
+ }
+);
+export const ENDPOINT_DESCRIPTION = i18n.translate(
+ 'xpack.securitySolution.getStarted.landingCards.box.endpoint.desc',
+ {
+ defaultMessage: 'Prevent, collect, detect and respond — all with Elastic Agent.',
+ }
+);
+
+export const SIEM_CARD_TITLE = i18n.translate(
+ 'xpack.securitySolution.getStarted.landingCards.box.siemCard.title',
+ {
+ defaultMessage: 'SIEM for the modern SOC',
+ }
+);
+export const SIEM_CARD_DESCRIPTION = i18n.translate(
+ 'xpack.securitySolution.getStarted.landingCards.box.siemCard.desc',
+ {
+ defaultMessage: 'Detect, investigate, and respond to evolving threats in your environment.',
+ }
+);
+
+export const CLOUD_CARD_TITLE = i18n.translate(
+ 'xpack.securitySolution.getStarted.landingCards.box.cloudCard.title',
+ {
+ defaultMessage: 'Cloud protection end-to-end',
+ }
+);
+export const CLOUD_CARD_DESCRIPTION = i18n.translate(
+ 'xpack.securitySolution.getStarted.landingCards.box.cloudCard.desc',
+ {
+ defaultMessage: 'Assess your Cloud Posture and protect your workloads from attacks.',
+ }
+);
+
+export const UNIFY_TITLE = i18n.translate(
+ 'xpack.securitySolution.getStarted.landingCards.box.unify.title',
+ {
+ defaultMessage: 'Unify SIEM, endpoint security, and cloud security',
+ }
+);
+export const UNIFY_DESCRIPTION = i18n.translate(
+ 'xpack.securitySolution.getStarted.landingCards.box.unify.desc',
+ {
+ defaultMessage:
+ 'Elastic Security modernizes security operations — enabling analytics across years of data, automating key processes, and protecting every host.',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/__mocks__/storage.ts b/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/__mocks__/storage.ts
index 87503ddc0b779..e02a83572a17f 100644
--- a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/__mocks__/storage.ts
+++ b/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/__mocks__/storage.ts
@@ -5,15 +5,13 @@
* 2.0.
*/
-export const onboardingStorage = {
- getAllFinishedStepsFromStorage: jest.fn(() => ({})),
- getFinishedStepsFromStorageByCardId: jest.fn(() => []),
- getActiveProductsFromStorage: jest.fn(() => []),
- toggleActiveProductsInStorage: jest.fn(() => []),
- resetAllExpandedCardStepsToStorage: jest.fn(),
- addFinishedStepToStorage: jest.fn(),
- removeFinishedStepFromStorage: jest.fn(),
- addExpandedCardStepToStorage: jest.fn(),
- removeExpandedCardStepFromStorage: jest.fn(),
- getAllExpandedCardStepsFromStorage: jest.fn(() => ({})),
-};
+export const mockGetAllFinishedStepsFromStorage = jest.fn(() => ({}));
+export const mockGetFinishedStepsFromStorageByCardId = jest.fn(() => []);
+export const mockGetActiveProductsFromStorage = jest.fn(() => []);
+export const mockToggleActiveProductsInStorage = jest.fn();
+export const mockResetAllExpandedCardStepsToStorage = jest.fn();
+export const mockAddFinishedStepToStorage = jest.fn();
+export const mockRemoveFinishedStepFromStorage = jest.fn();
+export const mockAddExpandedCardStepToStorage = jest.fn();
+export const mockRemoveExpandedCardStepFromStorage = jest.fn();
+export const mockGetAllExpandedCardStepsFromStorage = jest.fn(() => ({}));
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/card_step/content/video.tsx b/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/card_step/content/video.tsx
index 42c2b36ec96f6..23806c44ba0b6 100644
--- a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/card_step/content/video.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/card_step/content/video.tsx
@@ -8,6 +8,7 @@
import { EuiFlexGroup, EuiFlexItem, EuiIcon, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/react';
import React, { useCallback, useMemo } from 'react';
+import { VIDEO_SOURCE } from '../../../../empty_prompt/constants';
import { useStepContext } from '../../context/step_context';
import { WATCH_VIDEO_BUTTON_TITLE } from '../../translations';
import { OverviewSteps, QuickStartSectionCardsId, SectionId } from '../../types';
@@ -69,9 +70,7 @@ const VideoComponent: React.FC = () => {
sandbox="allow-scripts allow-same-origin"
scrolling="no"
allow={isVideoPlaying ? 'autoplay;' : undefined}
- src={`//play.vidyard.com/K6kKDBbP9SpXife9s2tHNP.html${
- isVideoPlaying ? '?autoplay=1' : ''
- }`}
+ src={`${VIDEO_SOURCE}${isVideoPlaying ? '?autoplay=1' : ''}`}
title={WATCH_VIDEO_BUTTON_TITLE}
/>
)}
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/hooks/use_toggle_panel.test.tsx b/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/hooks/use_toggle_panel.test.tsx
index 61fac46b03576..a5be7c9e74137 100644
--- a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/hooks/use_toggle_panel.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/hooks/use_toggle_panel.test.tsx
@@ -6,7 +6,6 @@
*/
import { renderHook, act } from '@testing-library/react-hooks';
import { useTogglePanel } from './use_toggle_panel';
-import { onboardingStorage } from '../storage';
import type { StepId } from '../types';
import {
@@ -23,8 +22,13 @@ import {
} from '../types';
import type { SecurityProductTypes } from '../configs';
import { ProductLine } from '../configs';
+import { OnboardingStorage } from '../storage';
+import * as mockStorage from '../__mocks__/storage';
+
+jest.mock('../storage', () => ({
+ OnboardingStorage: jest.fn(),
+}));
-jest.mock('../storage');
jest.mock('react-router-dom', () => ({
useLocation: jest.fn().mockReturnValue({ hash: '' }),
}));
@@ -54,12 +58,25 @@ describe('useTogglePanel', () => {
beforeEach(() => {
jest.clearAllMocks();
- (onboardingStorage.getAllFinishedStepsFromStorage as jest.Mock).mockReturnValue({
+ (OnboardingStorage as jest.Mock).mockImplementation(() => ({
+ getAllFinishedStepsFromStorage: mockStorage.mockGetAllFinishedStepsFromStorage,
+ getFinishedStepsFromStorageByCardId: mockStorage.mockGetFinishedStepsFromStorageByCardId,
+ getActiveProductsFromStorage: mockStorage.mockGetActiveProductsFromStorage,
+ toggleActiveProductsInStorage: mockStorage.mockToggleActiveProductsInStorage,
+ resetAllExpandedCardStepsToStorage: mockStorage.mockResetAllExpandedCardStepsToStorage,
+ addFinishedStepToStorage: mockStorage.mockAddFinishedStepToStorage,
+ removeFinishedStepFromStorage: mockStorage.mockRemoveFinishedStepFromStorage,
+ addExpandedCardStepToStorage: mockStorage.mockAddExpandedCardStepToStorage,
+ removeExpandedCardStepFromStorage: mockStorage.mockRemoveExpandedCardStepFromStorage,
+ getAllExpandedCardStepsFromStorage: mockStorage.mockGetAllExpandedCardStepsFromStorage,
+ }));
+
+ (mockStorage.mockGetAllFinishedStepsFromStorage as jest.Mock).mockReturnValue({
[QuickStartSectionCardsId.createFirstProject]: new Set([
CreateProjectSteps.createFirstProject,
]),
});
- (onboardingStorage.getActiveProductsFromStorage as jest.Mock).mockReturnValue([
+ (mockStorage.mockGetActiveProductsFromStorage as jest.Mock).mockReturnValue([
ProductLine.security,
ProductLine.cloud,
ProductLine.endpoint,
@@ -67,7 +84,7 @@ describe('useTogglePanel', () => {
});
test('should initialize state with correct initial values - when no active products from local storage', () => {
- (onboardingStorage.getActiveProductsFromStorage as jest.Mock).mockReturnValue([]);
+ (mockStorage.mockGetActiveProductsFromStorage as jest.Mock).mockReturnValue([]);
const { result } = renderHook(() => useTogglePanel({ productTypes, onboardingSteps }));
@@ -191,7 +208,7 @@ describe('useTogglePanel', () => {
});
test('should initialize state with correct initial values - when security product active', () => {
- (onboardingStorage.getActiveProductsFromStorage as jest.Mock).mockReturnValue([
+ (mockStorage.mockGetActiveProductsFromStorage as jest.Mock).mockReturnValue([
ProductLine.security,
]);
const { result } = renderHook(() => useTogglePanel({ productTypes, onboardingSteps }));
@@ -267,7 +284,7 @@ describe('useTogglePanel', () => {
});
});
- expect(onboardingStorage.resetAllExpandedCardStepsToStorage).toHaveBeenCalledTimes(1);
+ expect(mockStorage.mockResetAllExpandedCardStepsToStorage).toHaveBeenCalledTimes(1);
});
test('should add the current step to storage when it is expanded', () => {
@@ -284,8 +301,8 @@ describe('useTogglePanel', () => {
});
});
- expect(onboardingStorage.addExpandedCardStepToStorage).toHaveBeenCalledTimes(1);
- expect(onboardingStorage.addExpandedCardStepToStorage).toHaveBeenCalledWith(
+ expect(mockStorage.mockAddExpandedCardStepToStorage).toHaveBeenCalledTimes(1);
+ expect(mockStorage.mockAddExpandedCardStepToStorage).toHaveBeenCalledWith(
QuickStartSectionCardsId.watchTheOverviewVideo,
OverviewSteps.getToKnowElasticSecurity
);
@@ -305,8 +322,8 @@ describe('useTogglePanel', () => {
});
});
- expect(onboardingStorage.removeExpandedCardStepFromStorage).toHaveBeenCalledTimes(1);
- expect(onboardingStorage.removeExpandedCardStepFromStorage).toHaveBeenCalledWith(
+ expect(mockStorage.mockRemoveExpandedCardStepFromStorage).toHaveBeenCalledTimes(1);
+ expect(mockStorage.mockRemoveExpandedCardStepFromStorage).toHaveBeenCalledWith(
QuickStartSectionCardsId.watchTheOverviewVideo,
OverviewSteps.getToKnowElasticSecurity
);
@@ -325,8 +342,8 @@ describe('useTogglePanel', () => {
});
});
- expect(onboardingStorage.addFinishedStepToStorage).toHaveBeenCalledTimes(1);
- expect(onboardingStorage.addFinishedStepToStorage).toHaveBeenCalledWith(
+ expect(mockStorage.mockAddFinishedStepToStorage).toHaveBeenCalledTimes(1);
+ expect(mockStorage.mockAddFinishedStepToStorage).toHaveBeenCalledWith(
QuickStartSectionCardsId.watchTheOverviewVideo,
OverviewSteps.getToKnowElasticSecurity
);
@@ -346,8 +363,8 @@ describe('useTogglePanel', () => {
});
});
- expect(onboardingStorage.removeFinishedStepFromStorage).toHaveBeenCalledTimes(1);
- expect(onboardingStorage.removeFinishedStepFromStorage).toHaveBeenCalledWith(
+ expect(mockStorage.mockRemoveFinishedStepFromStorage).toHaveBeenCalledTimes(1);
+ expect(mockStorage.mockRemoveFinishedStepFromStorage).toHaveBeenCalledWith(
QuickStartSectionCardsId.watchTheOverviewVideo,
OverviewSteps.getToKnowElasticSecurity,
onboardingSteps
@@ -363,8 +380,8 @@ describe('useTogglePanel', () => {
onProductSwitchChanged({ id: ProductLine.security, label: 'Analytics' });
});
- expect(onboardingStorage.toggleActiveProductsInStorage).toHaveBeenCalledTimes(1);
- expect(onboardingStorage.toggleActiveProductsInStorage).toHaveBeenCalledWith(
+ expect(mockStorage.mockToggleActiveProductsInStorage).toHaveBeenCalledTimes(1);
+ expect(mockStorage.mockToggleActiveProductsInStorage).toHaveBeenCalledWith(
ProductLine.security
);
});
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/hooks/use_toggle_panel.tsx b/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/hooks/use_toggle_panel.tsx
index 81d4b6557239b..74d44c0dbdf35 100644
--- a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/hooks/use_toggle_panel.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/hooks/use_toggle_panel.tsx
@@ -11,7 +11,7 @@ import { useNavigateTo } from '@kbn/security-solution-navigation';
import { SecurityPageName } from '../../../../../../common';
-import { onboardingStorage } from '../storage';
+import { OnboardingStorage } from '../storage';
import {
getActiveSectionsInitialStates,
getActiveProductsInitialStates,
@@ -31,8 +31,12 @@ import { OnboardingActions } from '../types';
import { findCardSectionByStepId } from '../helpers';
import type { SecurityProductTypes } from '../configs';
import { ALL_PRODUCT_LINES, ProductLine } from '../configs';
+import { useSpaceId } from '../../../../hooks/use_space_id';
-const syncExpandedCardStepsToStorageFromURL = (maybeStepId: string) => {
+const syncExpandedCardStepsToStorageFromURL = (
+ onboardingStorage: OnboardingStorage,
+ maybeStepId: string
+) => {
const { matchedCard, matchedStep } = findCardSectionByStepId(maybeStepId);
const hasStepContent = matchedStep && matchedStep.description;
@@ -76,7 +80,9 @@ export const useTogglePanel = ({
const { hash: detailName } = useLocation();
const stepIdFromHash = detailName.split('#')[1];
+ const spaceId = useSpaceId();
+ const onboardingStorage = useMemo(() => new OnboardingStorage(spaceId), [spaceId]);
const {
getAllFinishedStepsFromStorage,
getActiveProductsFromStorage,
@@ -90,7 +96,10 @@ export const useTogglePanel = ({
} = onboardingStorage;
const finishedStepsInitialStates = useMemo(
- () => getFinishedStepsInitialStates({ finishedSteps: getAllFinishedStepsFromStorage() }),
+ () =>
+ getFinishedStepsInitialStates({
+ finishedSteps: getAllFinishedStepsFromStorage(),
+ }),
[getAllFinishedStepsFromStorage]
);
@@ -121,11 +130,11 @@ export const useTogglePanel = ({
const expandedCardsInitialStates: ExpandedCardSteps = useMemo(() => {
if (stepIdFromHash) {
- syncExpandedCardStepsToStorageFromURL(stepIdFromHash);
+ syncExpandedCardStepsToStorageFromURL(onboardingStorage, stepIdFromHash);
}
return getAllExpandedCardStepsFromStorage();
- }, [getAllExpandedCardStepsFromStorage, stepIdFromHash]);
+ }, [onboardingStorage, getAllExpandedCardStepsFromStorage, stepIdFromHash]);
const onStepClicked: OnStepClicked = useCallback(
({ stepId, cardId, isExpanded }) => {
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/images/launch.png b/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/images/launch.png
index a4c1535223b8e..c4d66d5573087 100644
Binary files a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/images/launch.png and b/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/images/launch.png differ
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/onboarding.test.tsx b/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/onboarding.test.tsx
index f42c735addc28..cd8e83ea6f0f0 100644
--- a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/onboarding.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/onboarding.test.tsx
@@ -69,7 +69,7 @@ describe('OnboardingComponent', () => {
const pageTitle = getByText('Hi Unknown!');
const subtitle = getByText(`Get started with Security`);
const description = getByText(
- `This area shows you everything you need to know. Feel free to explore all content. You can always come back later at any time.`
+ `This area shows you everything you need to know. Feel free to explore all content. You can always come back here at any time.`
);
expect(pageTitle).toBeInTheDocument();
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/storage.test.ts b/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/storage.test.ts
index 7130f7c614321..9fd952a9b56be 100644
--- a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/storage.test.ts
+++ b/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/storage.test.ts
@@ -10,7 +10,8 @@ import {
defaultExpandedCards,
EXPANDED_CARDS_STORAGE_KEY,
FINISHED_STEPS_STORAGE_KEY,
- onboardingStorage,
+ getStorageKeyBySpace,
+ OnboardingStorage,
} from './storage';
import {
AddIntegrationsSteps,
@@ -29,9 +30,9 @@ import { ProductLine } from './configs';
jest.mock('../../../lib/local_storage');
-describe('useStorage', () => {
+describe.each([['test'], [undefined]])('useStorage - spaceId: %s', (spaceId) => {
const mockStorage = storage as unknown as MockStorage;
-
+ const onboardingStorage = new OnboardingStorage(spaceId);
const onboardingSteps = [
CreateProjectSteps.createFirstProject,
OverviewSteps.getToKnowElasticSecurity,
@@ -48,8 +49,9 @@ describe('useStorage', () => {
it('should return the active products from storage', () => {
expect(onboardingStorage.getActiveProductsFromStorage()).toEqual([]);
+ const activeProductsStorageKey = getStorageKeyBySpace(ACTIVE_PRODUCTS_STORAGE_KEY, spaceId);
- mockStorage.set(ACTIVE_PRODUCTS_STORAGE_KEY, [ProductLine.security, ProductLine.endpoint]);
+ mockStorage.set(activeProductsStorageKey, [ProductLine.security, ProductLine.endpoint]);
expect(onboardingStorage.getActiveProductsFromStorage()).toEqual([
ProductLine.security,
ProductLine.endpoint,
@@ -57,26 +59,28 @@ describe('useStorage', () => {
});
it('should toggle active products in storage', () => {
+ const activeProductsStorageKey = getStorageKeyBySpace(ACTIVE_PRODUCTS_STORAGE_KEY, spaceId);
+
expect(onboardingStorage.toggleActiveProductsInStorage(ProductLine.security)).toEqual([
ProductLine.security,
]);
- expect(mockStorage.set).toHaveBeenCalledWith(ACTIVE_PRODUCTS_STORAGE_KEY, [
- ProductLine.security,
- ]);
+ expect(mockStorage.set).toHaveBeenCalledWith(activeProductsStorageKey, [ProductLine.security]);
- mockStorage.set(ACTIVE_PRODUCTS_STORAGE_KEY, [ProductLine.security]);
+ mockStorage.set(activeProductsStorageKey, [ProductLine.security]);
expect(onboardingStorage.toggleActiveProductsInStorage(ProductLine.security)).toEqual([]);
- expect(mockStorage.set).toHaveBeenCalledWith(ACTIVE_PRODUCTS_STORAGE_KEY, []);
+ expect(mockStorage.set).toHaveBeenCalledWith(activeProductsStorageKey, []);
});
it('should return the finished steps from storage by card ID', () => {
+ const finishedStepsStorageKey = getStorageKeyBySpace(FINISHED_STEPS_STORAGE_KEY, spaceId);
+
expect(
onboardingStorage.getFinishedStepsFromStorageByCardId(
QuickStartSectionCardsId.createFirstProject
)
).toEqual([CreateProjectSteps.createFirstProject]);
- mockStorage.set(FINISHED_STEPS_STORAGE_KEY, {
+ mockStorage.set(finishedStepsStorageKey, {
[QuickStartSectionCardsId.createFirstProject]: [
CreateProjectSteps.createFirstProject,
'step2',
@@ -91,9 +95,11 @@ describe('useStorage', () => {
});
it('should return all finished steps from storage', () => {
+ const finishedStepsStorageKey = getStorageKeyBySpace(FINISHED_STEPS_STORAGE_KEY, spaceId);
+
expect(onboardingStorage.getAllFinishedStepsFromStorage()).toEqual(DEFAULT_FINISHED_STEPS);
- mockStorage.set(FINISHED_STEPS_STORAGE_KEY, {
+ mockStorage.set(finishedStepsStorageKey, {
[QuickStartSectionCardsId.createFirstProject]: [
CreateProjectSteps.createFirstProject,
'step2',
@@ -110,22 +116,24 @@ describe('useStorage', () => {
});
it('should add a finished step to storage', () => {
+ const finishedStepsStorageKey = getStorageKeyBySpace(FINISHED_STEPS_STORAGE_KEY, spaceId);
+
onboardingStorage.addFinishedStepToStorage(
QuickStartSectionCardsId.createFirstProject,
CreateProjectSteps.createFirstProject
);
- expect(mockStorage.set).toHaveBeenCalledWith(FINISHED_STEPS_STORAGE_KEY, {
+ expect(mockStorage.set).toHaveBeenCalledWith(finishedStepsStorageKey, {
[QuickStartSectionCardsId.createFirstProject]: [CreateProjectSteps.createFirstProject],
});
- mockStorage.set(FINISHED_STEPS_STORAGE_KEY, {
+ mockStorage.set(finishedStepsStorageKey, {
[QuickStartSectionCardsId.createFirstProject]: [CreateProjectSteps.createFirstProject],
});
onboardingStorage.addFinishedStepToStorage(
QuickStartSectionCardsId.createFirstProject,
'step2' as StepId
);
- expect(mockStorage.set).toHaveBeenCalledWith(FINISHED_STEPS_STORAGE_KEY, {
+ expect(mockStorage.set).toHaveBeenCalledWith(finishedStepsStorageKey, {
[QuickStartSectionCardsId.createFirstProject]: [
CreateProjectSteps.createFirstProject,
'step2',
@@ -134,7 +142,9 @@ describe('useStorage', () => {
});
it('should get finished steps from storage by card ID', () => {
- mockStorage.set(FINISHED_STEPS_STORAGE_KEY, {
+ const finishedStepsStorageKey = getStorageKeyBySpace(FINISHED_STEPS_STORAGE_KEY, spaceId);
+
+ mockStorage.set(finishedStepsStorageKey, {
[QuickStartSectionCardsId.createFirstProject]: [
CreateProjectSteps.createFirstProject,
'step2',
@@ -156,7 +166,9 @@ describe('useStorage', () => {
});
it('should get all finished steps from storage', () => {
- mockStorage.set(FINISHED_STEPS_STORAGE_KEY, {
+ const finishedStepsStorageKey = getStorageKeyBySpace(FINISHED_STEPS_STORAGE_KEY, spaceId);
+
+ mockStorage.set(finishedStepsStorageKey, {
[QuickStartSectionCardsId.createFirstProject]: [
CreateProjectSteps.createFirstProject,
'step2',
@@ -174,23 +186,27 @@ describe('useStorage', () => {
card3: ['step4'],
});
- mockStorage.set(FINISHED_STEPS_STORAGE_KEY, {});
+ mockStorage.set(finishedStepsStorageKey, {});
expect(onboardingStorage.getAllFinishedStepsFromStorage()).toEqual(DEFAULT_FINISHED_STEPS);
});
it('should remove a finished step from storage', () => {
+ const finishedStepsStorageKey = getStorageKeyBySpace(FINISHED_STEPS_STORAGE_KEY, spaceId);
+
onboardingStorage.removeFinishedStepFromStorage(
QuickStartSectionCardsId.createFirstProject,
'step2' as StepId,
onboardingSteps
);
- expect(mockStorage.set).toHaveBeenCalledWith(FINISHED_STEPS_STORAGE_KEY, {
+ expect(mockStorage.set).toHaveBeenCalledWith(finishedStepsStorageKey, {
[QuickStartSectionCardsId.createFirstProject]: [CreateProjectSteps.createFirstProject],
});
});
it('should not remove a default finished step from storage', () => {
- mockStorage.set(FINISHED_STEPS_STORAGE_KEY, {
+ const finishedStepsStorageKey = getStorageKeyBySpace(FINISHED_STEPS_STORAGE_KEY, spaceId);
+
+ mockStorage.set(finishedStepsStorageKey, {
[QuickStartSectionCardsId.createFirstProject]: [
CreateProjectSteps.createFirstProject,
'step2',
@@ -202,7 +218,7 @@ describe('useStorage', () => {
CreateProjectSteps.createFirstProject,
onboardingSteps
);
- expect(mockStorage.get(FINISHED_STEPS_STORAGE_KEY)).toEqual({
+ expect(mockStorage.get(finishedStepsStorageKey)).toEqual({
[QuickStartSectionCardsId.createFirstProject]: [
CreateProjectSteps.createFirstProject,
'step2',
@@ -211,6 +227,8 @@ describe('useStorage', () => {
});
it('should get all expanded card steps from storage', () => {
+ const expandedCardsStorageKey = getStorageKeyBySpace(EXPANDED_CARDS_STORAGE_KEY, spaceId);
+
(mockStorage.get as jest.Mock).mockReturnValueOnce({
[QuickStartSectionCardsId.createFirstProject]: {
isExpanded: true,
@@ -218,7 +236,7 @@ describe('useStorage', () => {
},
});
const result = onboardingStorage.getAllExpandedCardStepsFromStorage();
- expect(mockStorage.get).toHaveBeenCalledWith(EXPANDED_CARDS_STORAGE_KEY);
+ expect(mockStorage.get).toHaveBeenCalledWith(expandedCardsStorageKey);
expect(result).toEqual({
[QuickStartSectionCardsId.createFirstProject]: {
isExpanded: true,
@@ -228,13 +246,17 @@ describe('useStorage', () => {
});
it('should get default expanded card steps from storage', () => {
+ const expandedCardsStorageKey = getStorageKeyBySpace(EXPANDED_CARDS_STORAGE_KEY, spaceId);
+
(mockStorage.get as jest.Mock).mockReturnValueOnce(null);
const result = onboardingStorage.getAllExpandedCardStepsFromStorage();
- expect(mockStorage.get).toHaveBeenCalledWith(EXPANDED_CARDS_STORAGE_KEY);
+ expect(mockStorage.get).toHaveBeenCalledWith(expandedCardsStorageKey);
expect(result).toEqual(defaultExpandedCards);
});
it('should reset card steps in storage', () => {
+ const expandedCardsStorageKey = getStorageKeyBySpace(EXPANDED_CARDS_STORAGE_KEY, spaceId);
+
(mockStorage.get as jest.Mock).mockReturnValueOnce({
[QuickStartSectionCardsId.watchTheOverviewVideo]: {
isExpanded: false,
@@ -242,7 +264,7 @@ describe('useStorage', () => {
},
});
onboardingStorage.resetAllExpandedCardStepsToStorage();
- expect(mockStorage.get(EXPANDED_CARDS_STORAGE_KEY)).toEqual({
+ expect(mockStorage.get(expandedCardsStorageKey)).toEqual({
[QuickStartSectionCardsId.watchTheOverviewVideo]: {
isExpanded: false,
expandedSteps: [],
@@ -251,7 +273,9 @@ describe('useStorage', () => {
});
it('should add a step to expanded card steps in storage', () => {
- mockStorage.set(EXPANDED_CARDS_STORAGE_KEY, {
+ const expandedCardsStorageKey = getStorageKeyBySpace(EXPANDED_CARDS_STORAGE_KEY, spaceId);
+
+ mockStorage.set(expandedCardsStorageKey, {
[QuickStartSectionCardsId.createFirstProject]: {
isExpanded: false,
expandedSteps: [],
@@ -265,7 +289,7 @@ describe('useStorage', () => {
QuickStartSectionCardsId.watchTheOverviewVideo,
OverviewSteps.getToKnowElasticSecurity
);
- expect(mockStorage.get(EXPANDED_CARDS_STORAGE_KEY)).toEqual({
+ expect(mockStorage.get(expandedCardsStorageKey)).toEqual({
[QuickStartSectionCardsId.createFirstProject]: {
isExpanded: false,
expandedSteps: [],
@@ -278,7 +302,9 @@ describe('useStorage', () => {
});
it('should remove a step from expanded card steps in storage', () => {
- mockStorage.set(EXPANDED_CARDS_STORAGE_KEY, {
+ const expandedCardsStorageKey = getStorageKeyBySpace(EXPANDED_CARDS_STORAGE_KEY, spaceId);
+
+ mockStorage.set(expandedCardsStorageKey, {
[QuickStartSectionCardsId.watchTheOverviewVideo]: {
isExpanded: true,
expandedSteps: [OverviewSteps.getToKnowElasticSecurity],
@@ -288,7 +314,7 @@ describe('useStorage', () => {
QuickStartSectionCardsId.watchTheOverviewVideo,
OverviewSteps.getToKnowElasticSecurity
);
- expect(mockStorage.get(EXPANDED_CARDS_STORAGE_KEY)).toEqual({
+ expect(mockStorage.get(expandedCardsStorageKey)).toEqual({
[QuickStartSectionCardsId.watchTheOverviewVideo]: {
isExpanded: false,
expandedSteps: [],
@@ -297,6 +323,8 @@ describe('useStorage', () => {
});
it('should update a card from expanded card steps in storage', () => {
+ const expandedCardsStorageKey = getStorageKeyBySpace(EXPANDED_CARDS_STORAGE_KEY, spaceId);
+
(mockStorage.get as jest.Mock).mockReturnValueOnce({
[QuickStartSectionCardsId.createFirstProject]: {
isExpanded: true,
@@ -306,7 +334,7 @@ describe('useStorage', () => {
onboardingStorage.removeExpandedCardStepFromStorage(
QuickStartSectionCardsId.createFirstProject
);
- expect(mockStorage.set).toHaveBeenCalledWith(EXPANDED_CARDS_STORAGE_KEY, {
+ expect(mockStorage.set).toHaveBeenCalledWith(expandedCardsStorageKey, {
[QuickStartSectionCardsId.createFirstProject]: {
isExpanded: false,
expandedSteps: [CreateProjectSteps.createFirstProject],
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/storage.ts b/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/storage.ts
index c7b036c7f294e..da52615ee283a 100644
--- a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/storage.ts
+++ b/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/storage.ts
@@ -22,6 +22,13 @@ export const ACTIVE_PRODUCTS_STORAGE_KEY = 'securitySolution.getStarted.activePr
export const FINISHED_STEPS_STORAGE_KEY = 'securitySolution.getStarted.finishedSteps';
export const EXPANDED_CARDS_STORAGE_KEY = 'securitySolution.getStarted.expandedCards';
+export const getStorageKeyBySpace = (storageKey: string, spaceId: string | null | undefined) => {
+ if (spaceId == null) {
+ return storageKey;
+ }
+ return `${storageKey}.${spaceId}`;
+};
+
export const defaultExpandedCards = {
[QuickStartSectionCardsId.watchTheOverviewVideo]: { isExpanded: false, expandedSteps: [] },
[QuickStartSectionCardsId.createFirstProject]: { isExpanded: false, expandedSteps: [] },
@@ -31,99 +38,127 @@ export const defaultExpandedCards = {
[GetStartedWithAlertsCardsId.viewAlerts]: { isExpanded: false, expandedSteps: [] },
};
-export const onboardingStorage = {
- setDefaultFinishedSteps: (cardId: CardId) => {
- const allFinishedSteps: Record = storage.get(FINISHED_STEPS_STORAGE_KEY);
+export class OnboardingStorage {
+ private finishedStepsStorageKey: string;
+ private activeProductsStorageKey: string;
+ private expandedCardsStorageKey: string;
+
+ constructor(spaceId: string | undefined) {
+ this.finishedStepsStorageKey = getStorageKeyBySpace(FINISHED_STEPS_STORAGE_KEY, spaceId);
+ this.activeProductsStorageKey = getStorageKeyBySpace(ACTIVE_PRODUCTS_STORAGE_KEY, spaceId);
+ this.expandedCardsStorageKey = getStorageKeyBySpace(EXPANDED_CARDS_STORAGE_KEY, spaceId);
+ }
+ setDefaultFinishedSteps = (cardId: CardId) => {
+ const finishedStepsStorageKey = this.finishedStepsStorageKey;
+
+ const allFinishedSteps: Record = storage.get(finishedStepsStorageKey);
+
const defaultFinishedStepsByCardId = DEFAULT_FINISHED_STEPS[cardId];
+
const hasDefaultFinishedSteps = defaultFinishedStepsByCardId != null;
if (!hasDefaultFinishedSteps) {
return;
}
- storage.set(FINISHED_STEPS_STORAGE_KEY, {
+ storage.set(finishedStepsStorageKey, {
...allFinishedSteps,
[cardId]: Array.from(
// dedupe card steps
new Set([...(defaultFinishedStepsByCardId ?? []), ...(allFinishedSteps[cardId] ?? [])])
),
});
- },
- getActiveProductsFromStorage: () => {
- const activeProducts: ProductLine[] = storage.get(ACTIVE_PRODUCTS_STORAGE_KEY);
+ };
+ public getActiveProductsFromStorage = () => {
+ const activeProductsStorageKey = this.activeProductsStorageKey;
+ const activeProducts: ProductLine[] = storage.get(activeProductsStorageKey);
return activeProducts ?? [];
- },
- toggleActiveProductsInStorage: (productId: ProductLine) => {
- const activeProducts: ProductLine[] = storage.get(ACTIVE_PRODUCTS_STORAGE_KEY) ?? [];
+ };
+ public toggleActiveProductsInStorage = (productId: ProductLine) => {
+ const activeProductsStorageKey = this.activeProductsStorageKey;
+ const activeProducts: ProductLine[] = storage.get(activeProductsStorageKey) ?? [];
const index = activeProducts.indexOf(productId);
if (index < 0) {
activeProducts.push(productId);
} else {
activeProducts.splice(index, 1);
}
- storage.set(ACTIVE_PRODUCTS_STORAGE_KEY, activeProducts);
+ storage.set(activeProductsStorageKey, activeProducts);
return activeProducts;
- },
- getFinishedStepsFromStorageByCardId: (cardId: CardId) => {
- const finishedSteps = onboardingStorage.getAllFinishedStepsFromStorage();
+ };
+ getFinishedStepsFromStorageByCardId = (cardId: CardId) => {
+ const finishedSteps = this.getAllFinishedStepsFromStorage();
const steps: StepId[] = finishedSteps[cardId] ?? [];
return steps;
- },
- getAllFinishedStepsFromStorage: () => {
- const allFinishedSteps: Record = storage.get(FINISHED_STEPS_STORAGE_KEY);
+ };
+ public getAllFinishedStepsFromStorage = () => {
+ const finishedStepsStorageKey = this.finishedStepsStorageKey;
+ const allFinishedSteps: Record = storage.get(finishedStepsStorageKey);
+
if (allFinishedSteps == null) {
- storage.set(FINISHED_STEPS_STORAGE_KEY, DEFAULT_FINISHED_STEPS);
+ storage.set(finishedStepsStorageKey, DEFAULT_FINISHED_STEPS);
} else {
getSections().forEach((section) => {
section.cards?.forEach((card) => {
- onboardingStorage.setDefaultFinishedSteps(card.id);
+ this.setDefaultFinishedSteps(card.id);
});
});
}
- return storage.get(FINISHED_STEPS_STORAGE_KEY);
- },
+ return storage.get(finishedStepsStorageKey);
+ };
- addFinishedStepToStorage: (cardId: CardId, stepId: StepId) => {
- const finishedSteps = onboardingStorage.getAllFinishedStepsFromStorage();
+ public addFinishedStepToStorage = (cardId: CardId, stepId: StepId) => {
+ const finishedStepsStorageKey = this.finishedStepsStorageKey;
+ const finishedSteps = this.getAllFinishedStepsFromStorage();
const card: StepId[] = finishedSteps[cardId] ?? [];
if (card.indexOf(stepId) < 0) {
card.push(stepId);
- storage.set(FINISHED_STEPS_STORAGE_KEY, { ...finishedSteps, [cardId]: card });
+ storage.set(finishedStepsStorageKey, { ...finishedSteps, [cardId]: card });
}
- },
- removeFinishedStepFromStorage: (cardId: CardId, stepId: StepId, onboardingSteps: StepId[]) => {
+ };
+ public removeFinishedStepFromStorage = (
+ cardId: CardId,
+ stepId: StepId,
+ onboardingSteps: StepId[]
+ ) => {
if (isDefaultFinishedCardStep(cardId, stepId, onboardingSteps)) {
return;
}
- const finishedSteps = onboardingStorage.getAllFinishedStepsFromStorage();
+ const finishedStepsStorageKey = this.finishedStepsStorageKey;
+
+ const finishedSteps = this.getAllFinishedStepsFromStorage();
const steps: StepId[] = finishedSteps[cardId] ?? [];
const index = steps.indexOf(stepId);
if (index >= 0) {
steps.splice(index, 1);
}
- storage.set(FINISHED_STEPS_STORAGE_KEY, { ...finishedSteps, [cardId]: steps });
- },
- getAllExpandedCardStepsFromStorage: () => {
- const storageData = storage.get(EXPANDED_CARDS_STORAGE_KEY);
+ storage.set(finishedStepsStorageKey, { ...finishedSteps, [cardId]: steps });
+ };
+ public getAllExpandedCardStepsFromStorage = () => {
+ const expandedCardsStorageKey = this.expandedCardsStorageKey;
+ const storageData = storage.get(expandedCardsStorageKey);
return !storageData || Object.keys(storageData).length === 0
? defaultExpandedCards
: storageData;
- },
- resetAllExpandedCardStepsToStorage: () => {
+ };
+ public resetAllExpandedCardStepsToStorage = () => {
const activeCards: Record =
- onboardingStorage.getAllExpandedCardStepsFromStorage();
+ this.getAllExpandedCardStepsFromStorage();
+ const expandedCardsStorageKey = this.expandedCardsStorageKey;
storage.set(
- EXPANDED_CARDS_STORAGE_KEY,
+ expandedCardsStorageKey,
Object.entries(activeCards).reduce((acc, [cardId, card]) => {
acc[cardId as CardId] = defaultExpandedCards[cardId as CardId] ?? card;
return acc;
}, {} as Record)
);
- },
- addExpandedCardStepToStorage: (cardId: CardId, stepId: StepId) => {
+ };
+ public addExpandedCardStepToStorage = (cardId: CardId, stepId: StepId) => {
const activeCards: Record =
- onboardingStorage.getAllExpandedCardStepsFromStorage();
+ this.getAllExpandedCardStepsFromStorage();
+ const expandedCardsStorageKey = this.expandedCardsStorageKey;
+
const card = activeCards[cardId]
? {
expandedSteps: [stepId],
@@ -134,13 +169,15 @@ export const onboardingStorage = {
expandedSteps: [],
};
- storage.set(EXPANDED_CARDS_STORAGE_KEY, { ...activeCards, [cardId]: card });
- },
- removeExpandedCardStepFromStorage: (cardId: CardId, stepId?: StepId) => {
+ storage.set(expandedCardsStorageKey, { ...activeCards, [cardId]: card });
+ };
+ public removeExpandedCardStepFromStorage = (cardId: CardId, stepId?: StepId) => {
+ const expandedCardsStorageKey = this.expandedCardsStorageKey;
+
const activeCards: Record<
CardId,
{ isExpanded: boolean; expandedSteps: StepId[] } | undefined
- > = storage.get(EXPANDED_CARDS_STORAGE_KEY) ?? {};
+ > = storage.get(expandedCardsStorageKey) ?? {};
const card = activeCards[cardId];
if (card && !stepId) {
card.isExpanded = false;
@@ -152,6 +189,6 @@ export const onboardingStorage = {
card.isExpanded = false;
}
}
- storage.set(EXPANDED_CARDS_STORAGE_KEY, { ...activeCards, [cardId]: card });
- },
-};
+ storage.set(expandedCardsStorageKey, { ...activeCards, [cardId]: card });
+ };
+}
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/styles/welcome_header.styles.ts b/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/styles/welcome_header.styles.ts
index 78b7c5bc9110d..0fe6ac75e49f2 100644
--- a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/styles/welcome_header.styles.ts
+++ b/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/styles/welcome_header.styles.ts
@@ -35,6 +35,7 @@ export const useWelcomeHeaderStyles = () => {
fontSize: `${euiTheme.base * 2.125}px`,
color: euiTheme.colors.title,
fontWeight: euiTheme.font.weight.bold,
+ lineHeight: euiTheme.size.xxl,
}),
headerDescriptionStyles: css({
fontSize: `${euiTheme.base}px`,
@@ -55,6 +56,7 @@ export const useWelcomeHeaderStyles = () => {
euiTheme.font.weight.regular,
euiTheme.size.l,
euiTheme.size.s,
+ euiTheme.size.xxl,
]);
return welcomeHeaderStyles;
};
diff --git a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/translations.ts b/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/translations.ts
index 81342be4c46dd..9193ffb17ea3a 100644
--- a/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/translations.ts
+++ b/x-pack/plugins/security_solution/public/common/components/landing_page/onboarding/translations.ts
@@ -23,7 +23,7 @@ export const GET_STARTED_PAGE_SUBTITLE = i18n.translate(
export const GET_STARTED_PAGE_DESCRIPTION = i18n.translate(
'xpack.securitySolution.onboarding.description',
{
- defaultMessage: `This area shows you everything you need to know. Feel free to explore all content. You can always come back later at any time.`,
+ defaultMessage: `This area shows you everything you need to know. Feel free to explore all content. You can always come back here at any time.`,
}
);
@@ -113,7 +113,7 @@ export const WATCH_VIDEO_DESCRIPTION2 = i18n.translate(
export const ADD_INTEGRATIONS_TITLE = i18n.translate(
'xpack.securitySolution.onboarding.step.addIntegrations.title',
{
- defaultMessage: 'Add integrations',
+ defaultMessage: 'Add data with integrations',
}
);
diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx
index 6e238e464a448..69e38c4e0cdf3 100644
--- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx
+++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx
@@ -80,7 +80,7 @@ import { MissingPrivilegesCallOut } from '../../components/callouts/missing_priv
import { useKibana } from '../../../common/lib/kibana';
import { NoPrivileges } from '../../../common/components/no_privileges';
import { HeaderPage } from '../../../common/components/header_page';
-import { LandingPageComponent } from '../../../common/components/landing_page';
+import { EmptyPrompt } from '../../../common/components/empty_prompt';
import type { FilterGroupHandler } from '../../../common/components/filter_group/types';
import type { Status } from '../../../../common/api/detection_engine';
import { AlertsTableFilterGroup } from '../../components/alerts_table/alerts_filter_group';
@@ -516,7 +516,7 @@ const DetectionEnginePageComponent: React.FC = ({
) : (
-
+
)}
>
);
diff --git a/x-pack/plugins/security_solution/public/entity_analytics/pages/entity_analytics_dashboard.tsx b/x-pack/plugins/security_solution/public/entity_analytics/pages/entity_analytics_dashboard.tsx
index 76efd79b4d63d..041d87b5f0e54 100644
--- a/x-pack/plugins/security_solution/public/entity_analytics/pages/entity_analytics_dashboard.tsx
+++ b/x-pack/plugins/security_solution/public/entity_analytics/pages/entity_analytics_dashboard.tsx
@@ -14,7 +14,7 @@ import { SecurityPageName } from '../../app/types';
import { useSourcererDataView } from '../../common/containers/sourcerer';
import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper';
import { HeaderPage } from '../../common/components/header_page';
-import { LandingPageComponent } from '../../common/components/landing_page';
+import { EmptyPrompt } from '../../common/components/empty_prompt';
import { SiemSearchBar } from '../../common/components/search_bar';
import { InputsModelId } from '../../common/store/inputs/constants';
import { FiltersGlobal } from '../../common/components/filters_global';
@@ -71,7 +71,7 @@ const EntityAnalyticsComponent = () => {
>
) : (
-
+
)}
diff --git a/x-pack/plugins/security_solution/public/explore/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/explore/hosts/pages/details/index.tsx
index 3bf714efaf866..41303bac31881 100644
--- a/x-pack/plugins/security_solution/public/explore/hosts/pages/details/index.tsx
+++ b/x-pack/plugins/security_solution/public/explore/hosts/pages/details/index.tsx
@@ -69,7 +69,7 @@ import { ID, useHostDetails } from '../../containers/hosts/details';
import { manageQuery } from '../../../../common/components/page/manage_query';
import { useInvalidFilterQuery } from '../../../../common/hooks/use_invalid_filter_query';
import { useSourcererDataView } from '../../../../common/containers/sourcerer';
-import { LandingPageComponent } from '../../../../common/components/landing_page';
+import { EmptyPrompt } from '../../../../common/components/empty_prompt';
import { AlertCountByRuleByStatus } from '../../../../common/components/alert_count_by_status';
import { useLicense } from '../../../../common/hooks/use_license';
import { ResponderActionButton } from '../../../../detections/components/endpoint_responder/responder_action_button';
@@ -309,7 +309,7 @@ const HostDetailsComponent: React.FC = ({ detailName, hostDeta
>
) : (
-
+
)}
diff --git a/x-pack/plugins/security_solution/public/explore/hosts/pages/hosts.test.tsx b/x-pack/plugins/security_solution/public/explore/hosts/pages/hosts.test.tsx
index 94ee4d6e1e28f..0359fa2a4609f 100644
--- a/x-pack/plugins/security_solution/public/explore/hosts/pages/hosts.test.tsx
+++ b/x-pack/plugins/security_solution/public/explore/hosts/pages/hosts.test.tsx
@@ -21,7 +21,7 @@ import { mockCasesContract } from '@kbn/cases-plugin/public/mocks';
import { InputsModelId } from '../../../common/store/inputs/constants';
jest.mock('../../../common/containers/sourcerer');
-jest.mock('../../../common/components/landing_page');
+jest.mock('../../../common/components/empty_prompt');
// Test will fail because we will to need to mock some core services to make the test work
// For now let's forget about SiemSearchBar and QueryBar
jest.mock('../../../common/components/search_bar', () => ({
@@ -96,7 +96,7 @@ describe('Hosts - rendering', () => {
);
- expect(wrapper.find('[data-test-subj="siem-landing-page"]').exists()).toBe(true);
+ expect(wrapper.find('[data-test-subj="empty-prompt"]').exists()).toBe(true);
});
test('it DOES NOT render the Setup Instructions text when an index is available', async () => {
diff --git a/x-pack/plugins/security_solution/public/explore/hosts/pages/hosts.tsx b/x-pack/plugins/security_solution/public/explore/hosts/pages/hosts.tsx
index 3310969294b0a..850a814f1503f 100644
--- a/x-pack/plugins/security_solution/public/explore/hosts/pages/hosts.tsx
+++ b/x-pack/plugins/security_solution/public/explore/hosts/pages/hosts.tsx
@@ -53,7 +53,7 @@ import { useSourcererDataView } from '../../../common/containers/sourcerer';
import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector';
import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query';
import { ID } from '../containers/hosts';
-import { LandingPageComponent } from '../../../common/components/landing_page';
+import { EmptyPrompt } from '../../../common/components/empty_prompt';
import { fieldNameExistsFilter } from '../../../common/components/visualization_actions/utils';
import { useLicense } from '../../../common/hooks/use_license';
import { useHasSecurityCapability } from '../../../helper_hooks';
@@ -239,7 +239,7 @@ const HostsComponent = () => {
) : (
-
+
)}
diff --git a/x-pack/plugins/security_solution/public/explore/network/pages/details/index.test.tsx b/x-pack/plugins/security_solution/public/explore/network/pages/details/index.test.tsx
index 0a26a241fdb02..5601aa95fe084 100644
--- a/x-pack/plugins/security_solution/public/explore/network/pages/details/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/explore/network/pages/details/index.test.tsx
@@ -101,7 +101,7 @@ jest.mock('../../../../common/components/search_bar', () => ({
jest.mock('../../../../common/components/query_bar', () => ({
QueryBar: () => null,
}));
-jest.mock('../../../../common/components/landing_page');
+jest.mock('../../../../common/components/empty_prompt');
const getMockHistory = (ip: string) => ({
length: 2,
@@ -204,6 +204,6 @@ describe('Network Details', () => {
);
- expect(wrapper.find('[data-test-subj="siem-landing-page"]').exists()).toBe(true);
+ expect(wrapper.find('[data-test-subj="empty-prompt"]').exists()).toBe(true);
});
});
diff --git a/x-pack/plugins/security_solution/public/explore/network/pages/details/index.tsx b/x-pack/plugins/security_solution/public/explore/network/pages/details/index.tsx
index 9b07b232ec050..f0356b9125dc3 100644
--- a/x-pack/plugins/security_solution/public/explore/network/pages/details/index.tsx
+++ b/x-pack/plugins/security_solution/public/explore/network/pages/details/index.tsx
@@ -42,7 +42,7 @@ import { networkModel } from '../../store';
import { SecurityPageName } from '../../../../app/types';
import { useSourcererDataView } from '../../../../common/containers/sourcerer';
import { useInvalidFilterQuery } from '../../../../common/hooks/use_invalid_filter_query';
-import { LandingPageComponent } from '../../../../common/components/landing_page';
+import { EmptyPrompt } from '../../../../common/components/empty_prompt';
import { TabNavigation } from '../../../../common/components/navigation/tab_navigation';
import { getNetworkDetailsPageFilter } from '../../../../common/components/visualization_actions/utils';
import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions';
@@ -260,7 +260,7 @@ const NetworkDetailsComponent: React.FC = () => {
>
) : (
-
+
)}
diff --git a/x-pack/plugins/security_solution/public/explore/network/pages/network.test.tsx b/x-pack/plugins/security_solution/public/explore/network/pages/network.test.tsx
index ebb8f759934bb..7ff298c95f81d 100644
--- a/x-pack/plugins/security_solution/public/explore/network/pages/network.test.tsx
+++ b/x-pack/plugins/security_solution/public/explore/network/pages/network.test.tsx
@@ -21,7 +21,7 @@ import { mockCasesContract } from '@kbn/cases-plugin/public/mocks';
import { InputsModelId } from '../../../common/store/inputs/constants';
-jest.mock('../../../common/components/landing_page');
+jest.mock('../../../common/components/empty_prompt');
jest.mock('../../../common/containers/sourcerer');
// Test will fail because we will to need to mock some core services to make the test work
@@ -127,7 +127,7 @@ describe('Network page - rendering', () => {
);
- expect(wrapper.find(`[data-test-subj="siem-landing-page"]`).exists()).toBe(true);
+ expect(wrapper.find(`[data-test-subj="empty-prompt"]`).exists()).toBe(true);
});
test('it DOES NOT render getting started page when an index is available', async () => {
diff --git a/x-pack/plugins/security_solution/public/explore/network/pages/network.tsx b/x-pack/plugins/security_solution/public/explore/network/pages/network.tsx
index 1caafa3ee182c..3ca4d2c7a4d2b 100644
--- a/x-pack/plugins/security_solution/public/explore/network/pages/network.tsx
+++ b/x-pack/plugins/security_solution/public/explore/network/pages/network.tsx
@@ -50,7 +50,7 @@ import { useSourcererDataView } from '../../../common/containers/sourcerer';
import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector';
import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query';
import { sourceOrDestinationIpExistsFilter } from '../../../common/components/visualization_actions/utils';
-import { LandingPageComponent } from '../../../common/components/landing_page';
+import { EmptyPrompt } from '../../../common/components/empty_prompt';
/**
* Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space.
*/
@@ -229,7 +229,7 @@ const NetworkComponent = React.memo(
) : (
-
+
)}
diff --git a/x-pack/plugins/security_solution/public/explore/users/pages/details/index.tsx b/x-pack/plugins/security_solution/public/explore/users/pages/details/index.tsx
index 63ad63c21251c..530892c2e8cc0 100644
--- a/x-pack/plugins/security_solution/public/explore/users/pages/details/index.tsx
+++ b/x-pack/plugins/security_solution/public/explore/users/pages/details/index.tsx
@@ -70,7 +70,7 @@ import { getCriteriaFromUsersType } from '../../../../common/components/ml/crite
import { UsersType } from '../../store/model';
import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions';
import { useMlCapabilities } from '../../../../common/components/ml/hooks/use_ml_capabilities';
-import { LandingPageComponent } from '../../../../common/components/landing_page';
+import { EmptyPrompt } from '../../../../common/components/empty_prompt';
import { useHasSecurityCapability } from '../../../../helper_hooks';
const QUERY_ID = 'UsersDetailsQueryId';
@@ -296,7 +296,7 @@ const UsersDetailsComponent: React.FC = ({
>
) : (
-
+
)}
diff --git a/x-pack/plugins/security_solution/public/explore/users/pages/users.tsx b/x-pack/plugins/security_solution/public/explore/users/pages/users.tsx
index 68318e77da152..fd0f7deff446c 100644
--- a/x-pack/plugins/security_solution/public/explore/users/pages/users.tsx
+++ b/x-pack/plugins/security_solution/public/explore/users/pages/users.tsx
@@ -50,7 +50,7 @@ import { generateSeverityFilter } from '../../hosts/store/helpers';
import { UsersTableType } from '../store/model';
import { hasMlUserPermissions } from '../../../../common/machine_learning/has_ml_user_permissions';
import { useMlCapabilities } from '../../../common/components/ml/hooks/use_ml_capabilities';
-import { LandingPageComponent } from '../../../common/components/landing_page';
+import { EmptyPrompt } from '../../../common/components/empty_prompt';
import { userNameExistsFilter } from './details/helpers';
import { useHasSecurityCapability } from '../../../helper_hooks';
@@ -230,7 +230,7 @@ const UsersComponent = () => {
) : (
-
+
)}
diff --git a/x-pack/plugins/security_solution/public/explore/users/pages/users_tabs.test.tsx b/x-pack/plugins/security_solution/public/explore/users/pages/users_tabs.test.tsx
index 49a2fff2a1ee0..2ec81611f3c96 100644
--- a/x-pack/plugins/security_solution/public/explore/users/pages/users_tabs.test.tsx
+++ b/x-pack/plugins/security_solution/public/explore/users/pages/users_tabs.test.tsx
@@ -16,7 +16,7 @@ import { Users } from './users';
import { useSourcererDataView } from '../../../common/containers/sourcerer';
import { mockCasesContext } from '@kbn/cases-plugin/public/mocks/mock_cases_context';
-jest.mock('../../../common/components/landing_page');
+jest.mock('../../../common/components/empty_prompt');
jest.mock('../../../common/containers/sourcerer');
jest.mock('../../../common/components/search_bar', () => ({
SiemSearchBar: () => null,
@@ -84,7 +84,7 @@ describe('Users - rendering', () => {
);
- expect(wrapper.find(`[data-test-subj="siem-landing-page"]`).exists()).toBe(true);
+ expect(wrapper.find(`[data-test-subj="empty-prompt"]`).exists()).toBe(true);
});
test('it should render tab navigation', async () => {
diff --git a/x-pack/plugins/security_solution/public/overview/pages/data_quality.test.tsx b/x-pack/plugins/security_solution/public/overview/pages/data_quality.test.tsx
index 3e075d27ac76b..23e44e8f651fe 100644
--- a/x-pack/plugins/security_solution/public/overview/pages/data_quality.test.tsx
+++ b/x-pack/plugins/security_solution/public/overview/pages/data_quality.test.tsx
@@ -17,7 +17,7 @@ import { useKibana } from '../../common/lib/kibana';
const mockedUseKibana = mockUseKibana();
-jest.mock('../../common/components/landing_page');
+jest.mock('../../common/components/empty_prompt');
jest.mock('../../common/lib/kibana', () => {
const original = jest.requireActual('../../common/lib/kibana');
@@ -117,7 +117,7 @@ describe('DataQuality', () => {
});
test('it does NOT render the landing page', () => {
- expect(screen.queryByTestId('siem-landing-page')).not.toBeInTheDocument();
+ expect(screen.queryByTestId('empty-prompt')).not.toBeInTheDocument();
});
});
@@ -149,7 +149,7 @@ describe('DataQuality', () => {
});
test('it does NOT render the landing page', () => {
- expect(screen.queryByTestId('siem-landing-page')).not.toBeInTheDocument();
+ expect(screen.queryByTestId('empty-prompt')).not.toBeInTheDocument();
});
});
@@ -181,7 +181,7 @@ describe('DataQuality', () => {
});
test('it does NOT render the landing page', () => {
- expect(screen.queryByTestId('siem-landing-page')).not.toBeInTheDocument();
+ expect(screen.queryByTestId('empty-prompt')).not.toBeInTheDocument();
});
});
@@ -218,7 +218,7 @@ describe('DataQuality', () => {
});
test('it renders the landing page', () => {
- expect(screen.getByTestId('siem-landing-page')).toBeInTheDocument();
+ expect(screen.getByTestId('empty-prompt')).toBeInTheDocument();
});
});
@@ -255,7 +255,7 @@ describe('DataQuality', () => {
});
test('it does NOT render the landing page', () => {
- expect(screen.queryByTestId('siem-landing-page')).not.toBeInTheDocument();
+ expect(screen.queryByTestId('empty-prompt')).not.toBeInTheDocument();
});
});
@@ -292,7 +292,7 @@ describe('DataQuality', () => {
});
test('it does NOT render the landing page', () => {
- expect(screen.queryByTestId('siem-landing-page')).not.toBeInTheDocument();
+ expect(screen.queryByTestId('empty-prompt')).not.toBeInTheDocument();
});
});
diff --git a/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx b/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx
index 1c2be8f8a798e..5c74c0e334180 100644
--- a/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx
+++ b/x-pack/plugins/security_solution/public/overview/pages/data_quality.tsx
@@ -35,7 +35,7 @@ import { SecurityPageName } from '../../app/types';
import { getGroupByFieldsOnClick } from '../../common/components/alerts_treemap/lib/helpers';
import { useThemes } from '../../common/components/charts/common';
import { HeaderPage } from '../../common/components/header_page';
-import { LandingPageComponent } from '../../common/components/landing_page';
+import { EmptyPrompt } from '../../common/components/empty_prompt';
import { useLocalStorage } from '../../common/components/local_storage';
import { SecuritySolutionPageWrapper } from '../../common/components/page_wrapper';
import { APP_ID, DEFAULT_BYTES_FORMAT, DEFAULT_NUMBER_FORMAT } from '../../../common/constants';
@@ -296,7 +296,7 @@ const DataQualityComponent: React.FC = () => {
/>
) : (
-
+
)}
diff --git a/x-pack/plugins/security_solution/public/overview/pages/detection_response.test.tsx b/x-pack/plugins/security_solution/public/overview/pages/detection_response.test.tsx
index 2b5561f58cb5c..352a8aa3744d8 100644
--- a/x-pack/plugins/security_solution/public/overview/pages/detection_response.test.tsx
+++ b/x-pack/plugins/security_solution/public/overview/pages/detection_response.test.tsx
@@ -45,7 +45,7 @@ jest.mock('../../common/components/filters_global', () => ({
FiltersGlobal: ({ children }: { children: React.ReactNode }) =>