diff --git a/x-pack/plugins/cases/public/components/create/flyout/create_case_flyout.test.tsx b/x-pack/plugins/cases/public/components/create/flyout/create_case_flyout.test.tsx
index df2ba80901738..effa3d450af89 100644
--- a/x-pack/plugins/cases/public/components/create/flyout/create_case_flyout.test.tsx
+++ b/x-pack/plugins/cases/public/components/create/flyout/create_case_flyout.test.tsx
@@ -44,4 +44,24 @@ describe('CreateCaseFlyout', () => {
});
expect(onClose).toBeCalled();
});
+
+ it('renders headerContent when passed', async () => {
+ const headerContent =
;
+ const { getByTestId } = mockedContext.render(
+
+ );
+
+ await act(async () => {
+ expect(getByTestId('testing123')).toBeTruthy();
+ expect(getByTestId('create-case-flyout-header').children.length).toEqual(2);
+ });
+ });
+
+ it('does not render headerContent when undefined', async () => {
+ const { getByTestId } = mockedContext.render();
+
+ await act(async () => {
+ expect(getByTestId('create-case-flyout-header').children.length).toEqual(1);
+ });
+ });
});
diff --git a/x-pack/plugins/cases/public/components/create/flyout/create_case_flyout.tsx b/x-pack/plugins/cases/public/components/create/flyout/create_case_flyout.tsx
index 75a18f2e70209..8f5e420f6b79d 100644
--- a/x-pack/plugins/cases/public/components/create/flyout/create_case_flyout.tsx
+++ b/x-pack/plugins/cases/public/components/create/flyout/create_case_flyout.tsx
@@ -25,6 +25,7 @@ export interface CreateCaseFlyoutProps {
onClose?: () => void;
onSuccess?: (theCase: Case) => Promise;
attachments?: CaseAttachmentsWithoutOwner;
+ headerContent?: React.ReactNode;
}
const StyledFlyout = styled(EuiFlyout)`
@@ -71,9 +72,10 @@ const FormWrapper = styled.div`
`;
export const CreateCaseFlyout = React.memo(
- ({ afterCaseCreated, onClose, onSuccess, attachments }) => {
+ ({ afterCaseCreated, onClose, onSuccess, attachments, headerContent }) => {
const handleCancel = onClose || function () {};
const handleOnSuccess = onSuccess || async function () {};
+
return (
@@ -83,10 +85,11 @@ export const CreateCaseFlyout = React.memo(
// maskProps is needed in order to apply the z-index to the parent overlay element, not to the flyout only
maskProps={{ className: maskOverlayClassName }}
>
-
+
{i18n.CREATE_CASE_TITLE}
+ {headerContent && headerContent}
diff --git a/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.tsx b/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.tsx
index 86b03f46bf745..a92046e8c9928 100644
--- a/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.tsx
+++ b/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.tsx
@@ -5,6 +5,7 @@
* 2.0.
*/
+import type React from 'react';
import { useCallback } from 'react';
import type { CaseAttachmentsWithoutOwner } from '../../../types';
import { useCasesToast } from '../../../common/use_cases_toast';
@@ -29,12 +30,16 @@ export const useCasesAddToNewCaseFlyout = (props: AddToNewCaseFlyoutProps = {})
}, [dispatch]);
const openFlyout = useCallback(
- ({ attachments }: { attachments?: CaseAttachmentsWithoutOwner } = {}) => {
+ ({
+ attachments,
+ headerContent,
+ }: { attachments?: CaseAttachmentsWithoutOwner; headerContent?: React.ReactNode } = {}) => {
dispatch({
type: CasesContextStoreActionsList.OPEN_CREATE_CASE_FLYOUT,
payload: {
...props,
attachments,
+ headerContent,
onClose: () => {
closeFlyout();
if (props.onClose) {
diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts
index aa581971e5f09..a92dde76777f5 100644
--- a/x-pack/plugins/security_solution/common/experimental_features.ts
+++ b/x-pack/plugins/security_solution/common/experimental_features.ts
@@ -64,6 +64,11 @@ export const allowedExperimentalValues = Object.freeze({
* Enables endpoint package level rbac
*/
endpointRbacEnabled: false,
+
+ /**
+ * Enables the Guided Onboarding tour in security
+ */
+ guidedOnboarding: false,
});
type ExperimentalConfigKeys = Array;
diff --git a/x-pack/plugins/security_solution/cypress/e2e/guided_onboarding/tour.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/guided_onboarding/tour.cy.ts
index 41e77e8aeac29..0339445bc8240 100644
--- a/x-pack/plugins/security_solution/cypress/e2e/guided_onboarding/tour.cy.ts
+++ b/x-pack/plugins/security_solution/cypress/e2e/guided_onboarding/tour.cy.ts
@@ -7,7 +7,6 @@
import { login, visit } from '../../tasks/login';
import { completeTour, goToNextStep, skipTour } from '../../tasks/guided_onboarding';
-import { SECURITY_TOUR_ACTIVE_KEY } from '../../../public/common/components/guided_onboarding';
import { OVERVIEW_URL } from '../../urls/navigation';
import {
WELCOME_STEP,
@@ -21,11 +20,11 @@ before(() => {
login();
});
-describe('Guided onboarding tour', () => {
+// need to redo these tests for new implementation
+describe.skip('Guided onboarding tour', () => {
describe('Tour is enabled', () => {
beforeEach(() => {
visit(OVERVIEW_URL);
- window.localStorage.setItem(SECURITY_TOUR_ACTIVE_KEY, 'true');
});
it('can be completed', () => {
diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json
index 4a6e3a105ee72..bddfc36c7d61d 100644
--- a/x-pack/plugins/security_solution/kibana.json
+++ b/x-pack/plugins/security_solution/kibana.json
@@ -19,6 +19,7 @@
"embeddable",
"eventLog",
"features",
+ "guidedOnboarding",
"inspector",
"kubernetesSecurity",
"lens",
diff --git a/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx b/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx
index 37efdce430317..c5d86011226c3 100644
--- a/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx
+++ b/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx
@@ -10,7 +10,7 @@ import {
EuiHeaderSection,
EuiHeaderSectionItem,
} from '@elastic/eui';
-import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import React, { useEffect, useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal';
import { i18n } from '@kbn/i18n';
@@ -28,7 +28,6 @@ import { timelineDefaults } from '../../../timelines/store/timeline/defaults';
import { timelineSelectors } from '../../../timelines/store/timeline';
import { useShallowEqualSelector } from '../../../common/hooks/use_selector';
import { getScopeFromPath, showSourcererByPath } from '../../../common/containers/sourcerer';
-import { useTourContext } from '../../../common/components/guided_onboarding';
const BUTTON_ADD_DATA = i18n.translate('xpack.securitySolution.globalHeader.buttonAddData', {
defaultMessage: 'Add integrations',
@@ -83,12 +82,6 @@ export const GlobalHeader = React.memo(
};
}, [portalNode, setHeaderActionMenu, theme.theme$]);
- const { isTourShown, endTour } = useTourContext();
- const closeOnboardingTourIfShown = useCallback(() => {
- if (isTourShown) {
- endTour();
- }
- }, [isTourShown, endTour]);
return (
@@ -105,7 +98,6 @@ export const GlobalHeader = React.memo(
data-test-subj="add-data"
href={href}
iconType="indexOpen"
- onClick={closeOnboardingTourIfShown}
>
{BUTTON_ADD_DATA}
diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx
index 36940cc055645..3711a990ef726 100644
--- a/x-pack/plugins/security_solution/public/app/home/index.tsx
+++ b/x-pack/plugins/security_solution/public/app/home/index.tsx
@@ -22,7 +22,7 @@ import { useUpgradeSecurityPackages } from '../../common/hooks/use_upgrade_secur
import { GlobalHeader } from './global_header';
import { ConsoleManager } from '../../management/components/console/components/console_manager';
-import { TourContextProvider } from '../../common/components/guided_onboarding';
+import { TourContextProvider } from '../../common/components/guided_onboarding_tour';
import { useUrlState } from '../../common/hooks/use_url_state';
import { useUpdateBrowserTitle } from '../../common/hooks/use_update_browser_title';
diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx
index a0ef3b8904e3f..edfe3e70e6ca0 100644
--- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx
@@ -7,19 +7,22 @@
import type { EuiTabbedContentTab } from '@elastic/eui';
import {
- EuiHorizontalRule,
- EuiTabbedContent,
- EuiSpacer,
- EuiLoadingContent,
- EuiNotificationBadge,
EuiFlexGroup,
EuiFlexItem,
+ EuiHorizontalRule,
+ EuiLoadingContent,
EuiLoadingSpinner,
+ EuiNotificationBadge,
+ EuiSpacer,
+ EuiTabbedContent,
} from '@elastic/eui';
import React, { useCallback, useMemo, useState } from 'react';
import styled from 'styled-components';
import { isEmpty } from 'lodash';
+import { GuidedOnboardingTourStep } from '../guided_onboarding_tour/tour_step';
+import { isDetectionsAlertsTable } from '../top_n/helpers';
+import { getTourAnchor, SecurityStepId } from '../guided_onboarding_tour/tour_config';
import type { AlertRawEventData } from './osquery_tab';
import { useOsqueryTab } from './osquery_tab';
import { EventFieldsBrowser } from './event_fields_browser';
@@ -179,6 +182,8 @@ const EventDetailsComponent: React.FC = ({
[detailsEcsData]
);
+ const isTourAnchor = useMemo(() => isDetectionsAlertsTable(scopeId), [scopeId]);
+
const showThreatSummary = useMemo(() => {
const hasEnrichments = enrichmentCount > 0;
const hasRiskInfoWithLicense = isLicenseValid && (hostRisk || userRisk);
@@ -401,14 +406,26 @@ const EventDetailsComponent: React.FC = ({
[tabs, selectedTabId]
);
+ const tourAnchor = useMemo(
+ () => (isTourAnchor ? { 'tour-step': getTourAnchor(3, SecurityStepId.alertsCases) } : {}),
+ [isTourAnchor]
+ );
+
return (
-
+
+
+
);
};
EventDetailsComponent.displayName = 'EventDetailsComponent';
diff --git a/x-pack/plugins/security_solution/public/common/components/guided_onboarding/tour.test.tsx b/x-pack/plugins/security_solution/public/common/components/guided_onboarding/tour.test.tsx
deleted file mode 100644
index e2cf3be0ae07d..0000000000000
--- a/x-pack/plugins/security_solution/public/common/components/guided_onboarding/tour.test.tsx
+++ /dev/null
@@ -1,71 +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 { act, renderHook } from '@testing-library/react-hooks';
-
-import {
- SECURITY_TOUR_ACTIVE_KEY,
- SECURITY_TOUR_STEP_KEY,
- TourContextProvider,
- useTourContext,
-} from './tour';
-
-describe('useTourContext', () => {
- describe('localStorage', () => {
- let localStorageTourActive: string | null;
- let localStorageTourStep: string | null;
-
- beforeAll(() => {
- localStorageTourActive = localStorage.getItem(SECURITY_TOUR_ACTIVE_KEY);
- localStorage.removeItem(SECURITY_TOUR_ACTIVE_KEY);
- localStorageTourStep = localStorage.getItem(SECURITY_TOUR_STEP_KEY);
- localStorage.removeItem(SECURITY_TOUR_STEP_KEY);
- });
-
- afterAll(() => {
- if (localStorageTourActive) {
- localStorage.setItem(SECURITY_TOUR_ACTIVE_KEY, localStorageTourActive);
- }
- if (localStorageTourStep) {
- localStorage.setItem(SECURITY_TOUR_STEP_KEY, localStorageTourStep);
- }
- });
-
- test('tour is disabled', () => {
- localStorage.setItem(SECURITY_TOUR_ACTIVE_KEY, JSON.stringify(false));
- const { result } = renderHook(() => useTourContext(), {
- wrapper: TourContextProvider,
- });
- expect(result.current.isTourShown).toBe(false);
- });
-
- test('tour is enabled', () => {
- localStorage.setItem(SECURITY_TOUR_ACTIVE_KEY, JSON.stringify(true));
- const { result } = renderHook(() => useTourContext(), {
- wrapper: TourContextProvider,
- });
- expect(result.current.isTourShown).toBe(true);
- });
- test('endTour callback', () => {
- localStorage.setItem(SECURITY_TOUR_ACTIVE_KEY, JSON.stringify(true));
- let { result } = renderHook(() => useTourContext(), {
- wrapper: TourContextProvider,
- });
- expect(result.current.isTourShown).toBe(true);
- act(() => {
- result.current.endTour();
- });
- const localStorageValue = JSON.parse(localStorage.getItem(SECURITY_TOUR_ACTIVE_KEY)!);
- expect(localStorageValue).toBe(false);
-
- ({ result } = renderHook(() => useTourContext(), {
- wrapper: TourContextProvider,
- }));
- expect(result.current.isTourShown).toBe(false);
- });
- });
-});
diff --git a/x-pack/plugins/security_solution/public/common/components/guided_onboarding/tour.tsx b/x-pack/plugins/security_solution/public/common/components/guided_onboarding/tour.tsx
deleted file mode 100644
index 27288bb8a7145..0000000000000
--- a/x-pack/plugins/security_solution/public/common/components/guided_onboarding/tour.tsx
+++ /dev/null
@@ -1,189 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import type { ReactChild } from 'react';
-import React, { createContext, useContext, useState, useCallback } from 'react';
-
-import type { EuiTourStepProps } from '@elastic/eui';
-import {
- EuiButton,
- EuiButtonEmpty,
- EuiFlexGroup,
- EuiFlexItem,
- EuiTourStep,
- EuiText,
- EuiSpacer,
- EuiImage,
- useIsWithinBreakpoints,
-} from '@elastic/eui';
-import { FormattedMessage } from '@kbn/i18n-react';
-
-import type { StepConfig } from './tour_config';
-import { tourConfig } from './tour_config';
-
-export const SECURITY_TOUR_ACTIVE_KEY = 'guidedOnboarding.security.tourActive';
-export const SECURITY_TOUR_STEP_KEY = 'guidedOnboarding.security.tourStep';
-const getIsTourActiveFromLocalStorage = (): boolean => {
- const localStorageValue = localStorage.getItem(SECURITY_TOUR_ACTIVE_KEY);
- return localStorageValue ? JSON.parse(localStorageValue) : false;
-};
-export const saveIsTourActiveToLocalStorage = (isTourActive: boolean): void => {
- localStorage.setItem(SECURITY_TOUR_ACTIVE_KEY, JSON.stringify(isTourActive));
-};
-
-export const getTourStepFromLocalStorage = (): number => {
- return Number(localStorage.getItem(SECURITY_TOUR_STEP_KEY) ?? 1);
-};
-const saveTourStepToLocalStorage = (step: number): void => {
- localStorage.setItem(SECURITY_TOUR_STEP_KEY, JSON.stringify(step));
-};
-
-const minWidth: EuiTourStepProps['minWidth'] = 360;
-const maxWidth: EuiTourStepProps['maxWidth'] = 360;
-const offset: EuiTourStepProps['offset'] = 20;
-const repositionOnScroll: EuiTourStepProps['repositionOnScroll'] = true;
-
-const getSteps = (tourControls: {
- activeStep: number;
- incrementStep: () => void;
- resetTour: () => void;
-}) => {
- const { activeStep, incrementStep, resetTour } = tourControls;
- const footerAction = (
-
-
- resetTour()}
- data-test-subj="onboarding--securityTourSkipButton"
- >
-
-
-
-
- incrementStep()}
- color="success"
- data-test-subj="onboarding--securityTourNextStepButton"
- >
-
-
-
-
- );
- const lastStepFooter = (
- resetTour()}
- data-test-subj="onboarding--securityTourEndButton"
- >
-
-
- );
- return tourConfig.map((stepConfig: StepConfig) => {
- const { content, imageConfig, dataTestSubj, ...rest } = stepConfig;
- return (
- resetTour()}
- panelProps={{
- 'data-test-subj': dataTestSubj,
- }}
- content={
- <>
-
- {content}
-
- {imageConfig && (
- <>
-
-
- >
- )}
- >
- }
- footerAction={activeStep === tourConfig.length ? lastStepFooter : footerAction}
- />
- );
- });
-};
-
-export interface TourContextValue {
- isTourShown: boolean;
- endTour: () => void;
-}
-
-const TourContext = createContext({
- isTourShown: false,
- endTour: () => {},
-} as TourContextValue);
-
-export const TourContextProvider = ({ children }: { children: ReactChild }) => {
- const [isTourActive, _setIsTourActive] = useState(getIsTourActiveFromLocalStorage());
- const setIsTourActive = useCallback((value: boolean) => {
- _setIsTourActive(value);
- saveIsTourActiveToLocalStorage(value);
- }, []);
-
- const [activeStep, _setActiveStep] = useState(getTourStepFromLocalStorage());
-
- const incrementStep = useCallback(() => {
- _setActiveStep((prevState) => {
- const nextStep = (prevState >= tourConfig.length ? 0 : prevState) + 1;
- saveTourStepToLocalStorage(nextStep);
- return nextStep;
- });
- }, []);
-
- const resetStep = useCallback(() => {
- _setActiveStep(1);
- saveTourStepToLocalStorage(1);
- }, []);
-
- const resetTour = useCallback(() => {
- setIsTourActive(false);
- resetStep();
- }, [setIsTourActive, resetStep]);
-
- const isSmallScreen = useIsWithinBreakpoints(['xs', 's']);
- const showTour = isTourActive && !isSmallScreen;
- const context: TourContextValue = { isTourShown: showTour, endTour: resetTour };
- return (
-
- <>
- {children}
- {showTour && <>{getSteps({ activeStep, incrementStep, resetTour })}>}
- >
-
- );
-};
-
-export const useTourContext = (): TourContextValue => {
- const ctx = useContext(TourContext);
- if (!ctx) {
- throw new Error('useTourContext can only be called inside of TourContext!');
- }
- return ctx;
-};
diff --git a/x-pack/plugins/security_solution/public/common/components/guided_onboarding/tour_config.ts b/x-pack/plugins/security_solution/public/common/components/guided_onboarding/tour_config.ts
deleted file mode 100644
index 5a0f6f30daadc..0000000000000
--- a/x-pack/plugins/security_solution/public/common/components/guided_onboarding/tour_config.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0; you may not use this file except in compliance with the Elastic License
- * 2.0.
- */
-
-import type { EuiTourStepProps } from '@elastic/eui';
-import { i18n } from '@kbn/i18n';
-import alertsGif from '../../images/onboarding_tour_step_alerts.gif';
-import casesGif from '../../images/onboarding_tour_step_cases.gif';
-
-export type StepConfig = Pick & {
- anchor: string;
- dataTestSubj: string;
- imageConfig?: {
- altText: string;
- src: string;
- };
-};
-
-type TourConfig = StepConfig[];
-
-export const tourConfig: TourConfig = [
- {
- step: 1,
- title: i18n.translate('xpack.securitySolution.guided_onboarding.tour.overviewStep.tourTitle', {
- defaultMessage: 'Welcome to Elastic Security',
- }),
- content: i18n.translate(
- 'xpack.securitySolution.guided_onboarding.tour.overviewStep.tourContent',
- {
- defaultMessage:
- 'Take a quick tour to explore a unified workflow for investigating suspicious activity.',
- }
- ),
- anchor: `[id^="SolutionNav"]`,
- anchorPosition: 'rightUp',
- dataTestSubj: 'welcomeStep',
- },
- {
- step: 2,
- title: i18n.translate('xpack.securitySolution.guided_onboarding.tour.manageStep.tourTitle', {
- defaultMessage: 'Protect your ecosystem',
- }),
- content: i18n.translate(
- 'xpack.securitySolution.guided_onboarding.tour.manageStep.tourContent',
- {
- defaultMessage:
- 'Decide what matters to you and your environment and create rules to detect and prevent malicious activity. ',
- }
- ),
- anchor: `[data-test-subj="groupedNavItemLink-administration"]`,
- anchorPosition: 'rightUp',
- dataTestSubj: 'manageStep',
- },
- {
- step: 3,
- title: i18n.translate('xpack.securitySolution.guided_onboarding.tour.alertsStep.tourTitle', {
- defaultMessage: 'Get notified when something changes',
- }),
- content: i18n.translate(
- 'xpack.securitySolution.guided_onboarding.tour.alertsStep.tourContent',
- {
- defaultMessage:
- "Know when a rule's conditions are met, so you can start your investigation right away. Set up notifications with third-party platforms like Slack, PagerDuty, and ServiceNow.",
- }
- ),
- anchor: `[data-test-subj="groupedNavItemLink-alerts"]`,
- anchorPosition: 'rightUp',
- imageConfig: {
- src: alertsGif,
- altText: i18n.translate(
- 'xpack.securitySolution.guided_onboarding.tour.alertsStep.imageAltText',
- {
- defaultMessage: 'Alerts demonstration',
- }
- ),
- },
- dataTestSubj: 'alertsStep',
- },
- {
- step: 4,
- title: i18n.translate('xpack.securitySolution.guided_onboarding.tour.casesStep.tourTitle', {
- defaultMessage: 'Create a case to track your investigation',
- }),
- content: i18n.translate('xpack.securitySolution.guided_onboarding.tour.casesStep.tourContent', {
- defaultMessage:
- 'Collect evidence, add more collaborators, and even push case details to third-party case management systems.',
- }),
- anchor: `[data-test-subj="groupedNavItemLink-cases"]`,
- anchorPosition: 'rightUp',
- imageConfig: {
- src: casesGif,
- altText: i18n.translate(
- 'xpack.securitySolution.guided_onboarding.tour.casesStep.imageAltText',
- {
- defaultMessage: 'Cases demonstration',
- }
- ),
- },
- dataTestSubj: 'casesStep',
- },
- {
- step: 5,
- title: i18n.translate('xpack.securitySolution.guided_onboarding.tour.dataStep.tourTitle', {
- defaultMessage: `Start gathering your data!`,
- }),
- content: i18n.translate('xpack.securitySolution.guided_onboarding.tour.dataStep.tourContent', {
- defaultMessage: `Collect data from your endpoints using the Elastic Agent and a variety of third-party integrations.`,
- }),
- anchor: `[data-test-subj="add-data"]`,
- anchorPosition: 'rightUp',
- dataTestSubj: 'dataStep',
- },
-];
diff --git a/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/README.md b/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/README.md
new file mode 100644
index 0000000000000..eb30e20f1318e
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/README.md
@@ -0,0 +1,140 @@
+## Security Guided Onboarding Tour
+This work required some creativity for reasons. Allow me to explain some weirdness
+
+The [`EuiTourStep`](https://elastic.github.io/eui/#/display/tour) component needs an **anchor** to attach on in the DOM. This can be defined in 2 ways:
+```
+type EuiTourStepAnchorProps = ExclusiveUnion<{
+ //Element to which the tour step popover attaches when open
+ children: ReactElement;
+ // Selector or reference to the element to which the tour step popover attaches when open
+ anchor?: never;
+}, {
+ children?: never;
+ anchor: ElementTarget;
+}>;
+```
+
+It was important that the `EuiTourStep` **anchor** is in the DOM when the tour step becomes active. Additionally, when the **anchor** leaves the DOM, we need `EuiTourStep` to leave the DOM as well.
+
+## How to use components (for OLM/D&R step engineers)
+
+- Define your steps in [`./tour_config.ts`](https://github.com/elastic/kibana/pull/143598/files#diff-2c0372fc996eadbff00dddb92101432bf38cc1613895cb9a208abd8eb2e12930R136) in the `securityTourConfig` const
+- For each step, implement the `GuidedOnboardingTourStep` component at the location of the **anchor**. As stated in the previous section, there are two ways to define the **anchor**. I will explain examples of both methods:
+
+1. **Method 1 - as children.** Looking at step 1 of the `SecurityStepId.alertsCases` tour. In the `alertsCasesConfig` you can see the config for this step looks like:
+
+ ```
+ {
+ ...defaultConfig,
+ step: 1,
+ title: i18n.translate('xpack.securitySolution.guided_onboarding.tour.ruleNameStep.tourTitle', {
+ defaultMessage: 'Test alert for practice',
+ }),
+ content: i18n.translate(
+ 'xpack.securitySolution.guided_onboarding.tour.ruleNameStep.tourContent',
+ {
+ defaultMessage:
+ 'To help you practice triaging alerts, we enabled a rule to create your first alert.',
+ }
+ ),
+ anchorPosition: 'downCenter',
+ dataTestSubj: getTourAnchor(1, SecurityStepId.alertsCases),
+ }
+ ```
+
+ Notice that **no anchor prop is defined** in the step 1 config.
+ As you can see pictured below, the tour step anchor is the Rule name of the first alert.
+
+
+
+ The component for this anchor is `RenderCellValue` which returns `DefaultCellRenderer`. We wrap `DefaultCellRenderer` with `GuidedOnboardingTourStep`, passing `step={1} stepId={SecurityStepId.alertsCases}` to indicate the step. Since there are many other iterations of this component on the page, we also need to pass the `isTourAnchor` property to determine which of these components should be the anchor. In the code, this looks something like:
+
+ ```
+ export const RenderCellValue = (props) => {
+ const { columnId, rowIndex, scopeId } = props;
+ const isTourAnchor = useMemo(
+ () =>
+ columnId === SIGNAL_RULE_NAME_FIELD_NAME &&
+ isDetectionsAlertsTable(scopeId) &&
+ rowIndex === 0,
+ [columnId, rowIndex, scopeId]
+ );
+
+ return (
+
+
+
+ );
+ };
+ ```
+
+2. **Method 2 - as anchor props.** Looking at step 5 of the `SecurityStepId.alertsCases` tour. In the `alertsCasesConfig` you can see the config for this step looks like:
+
+ ```
+ {
+ ...defaultConfig,
+ step: 5,
+ title: i18n.translate('xpack.securitySolution.guided_onboarding.tour.createCase.tourTitle', {
+ defaultMessage: `Add details`,
+ }),
+ content: i18n.translate(
+ 'xpack.securitySolution.guided_onboarding.tour.createCase.tourContent',
+ {
+ defaultMessage: `In addition to the alert, you can add any relevant information you need to the case.`,
+ }
+ ),
+ anchor: `[data-test-subj="create-case-flyout"]`,
+ anchorPosition: 'leftUp',
+ dataTestSubj: getTourAnchor(5, SecurityStepId.alertsCases),
+ hideNextButton: true,
+ }
+ ```
+
+ Notice that the **anchor prop is defined** as `[data-test-subj="create-case-flyout"]` in the step 5 config. There is also a `hideNextButton` boolean utilized here.
+ As you can see pictured below, the tour step anchor is the create case flyout and the next button is hidden.
+
+
+
+
+ Since cases is its own plugin and we are using a method to generate the flyout, we cannot wrap the flyout as children of the `GuidedOnboardingTourStep`. We do however need the `EuiTourStep` component to mount in the same location as the anchor. Therefore, I had to pass a new optional property to the case component called `headerContent` that simply accepts and renders ` React.ReactNode` at the top of the flyout. In the code, this looks something like:
+
+ ```
+ createCaseFlyout.open({
+ attachments: caseAttachments,
+ ...(isTourShown(SecurityStepId.alertsCases) && activeStep === 4
+ ? {
+ headerContent: (
+ // isTourAnchor=true no matter what in order to
+ // force active guide step outside of security solution (cases)
+
+ ),
+ }
+ : {}),
+ });
+ ```
+
+- The **`useTourContext`** is used within anchor components, returning the state of the security tour
+ ```
+ export interface TourContextValue {
+ activeStep: number;
+ endTourStep: (stepId: SecurityStepId) => void;
+ incrementStep: (stepId: SecurityStepId, step?: number) => void;
+ isTourShown: (stepId: SecurityStepId) => boolean;
+ }
+ ```
+ When the tour step does not have a next button, the anchor component will need to call `incrementStep` after an action is taken. For example, in `SecurityStepId.alertsCases` step 4, the user needs to click the "Add to case" button to advance the tour.
+
+
+
+ So we utilize the `useTourContext` to do the following check and increment the step in `handleAddToNewCaseClick`:
+ ```
+ if (isTourShown(SecurityStepId.alertsCases) && activeStep === 4) {
+ incrementStep(SecurityStepId.alertsCases);
+ }
+ ```
+
+ In `SecurityStepId.alertsCases` step 5, the user needs to fill out the form and hit the "Create case" button in order to end the `alertsCases` portion the tour, so with the `afterCaseCreated` method we call `endTourStep(SecurityStepId.alertsCases)`.
\ No newline at end of file
diff --git a/x-pack/plugins/security_solution/public/common/components/guided_onboarding/index.ts b/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/index.ts
similarity index 67%
rename from x-pack/plugins/security_solution/public/common/components/guided_onboarding/index.ts
rename to x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/index.ts
index ed0dfa6c76339..2bb68dff4646f 100644
--- a/x-pack/plugins/security_solution/public/common/components/guided_onboarding/index.ts
+++ b/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/index.ts
@@ -5,9 +5,4 @@
* 2.0.
*/
-export {
- useTourContext,
- TourContextProvider,
- SECURITY_TOUR_ACTIVE_KEY,
- SECURITY_TOUR_STEP_KEY,
-} from './tour';
+export { useTourContext, TourContextProvider } from './tour';
diff --git a/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/tour.test.tsx b/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/tour.test.tsx
new file mode 100644
index 0000000000000..faea94a1c37ec
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/tour.test.tsx
@@ -0,0 +1,100 @@
+/*
+ * 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 { act, renderHook } from '@testing-library/react-hooks';
+import { of } from 'rxjs';
+import { TourContextProvider, useTourContext } from './tour';
+import { SecurityStepId, securityTourConfig } from './tour_config';
+import { useKibana } from '../../lib/kibana';
+
+jest.mock('../../lib/kibana');
+jest.mock('../../hooks/use_experimental_features', () => ({
+ useIsExperimentalFeatureEnabled: () => true,
+}));
+
+jest.mock('react-router-dom', () => {
+ const original = jest.requireActual('react-router-dom');
+
+ return {
+ ...original,
+ useLocation: jest.fn().mockReturnValue({ pathname: '/alerts' }),
+ };
+});
+
+describe('useTourContext', () => {
+ const mockCompleteGuideStep = jest.fn();
+ beforeEach(() => {
+ (useKibana as jest.Mock).mockReturnValue({
+ services: {
+ guidedOnboarding: {
+ guidedOnboardingApi: {
+ isGuideStepActive$: () => of(true),
+ completeGuideStep: mockCompleteGuideStep,
+ },
+ },
+ },
+ });
+ jest.clearAllMocks();
+ });
+ // @ts-ignore
+ const stepIds = Object.values(SecurityStepId);
+ describe.each(stepIds)('%s', (stepId) => {
+ it('if guidedOnboardingApi?.isGuideStepActive$ is false, isTourShown should be false', () => {
+ (useKibana as jest.Mock).mockReturnValue({
+ services: {
+ guidedOnboarding: {
+ guidedOnboardingApi: {
+ isGuideStepActive$: () => of(false),
+ },
+ },
+ },
+ });
+ const { result } = renderHook(() => useTourContext(), {
+ wrapper: TourContextProvider,
+ });
+ expect(result.current.isTourShown(stepId)).toBe(false);
+ });
+ it('if guidedOnboardingApi?.isGuideStepActive$ is true, isTourShown should be true', () => {
+ const { result } = renderHook(() => useTourContext(), {
+ wrapper: TourContextProvider,
+ });
+ expect(result.current.isTourShown(stepId)).toBe(true);
+ });
+ it('endTourStep calls completeGuideStep with correct stepId', async () => {
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook(() => useTourContext(), {
+ wrapper: TourContextProvider,
+ });
+ await waitForNextUpdate();
+ result.current.endTourStep(stepId);
+ expect(mockCompleteGuideStep).toHaveBeenCalledWith('security', stepId);
+ });
+ });
+ it('activeStep is initially 1', () => {
+ const { result } = renderHook(() => useTourContext(), {
+ wrapper: TourContextProvider,
+ });
+ expect(result.current.activeStep).toBe(1);
+ });
+ it('increment step properly increments for each stepId, and if attempted to increment beyond length of tour config steps resets activeStep to 1', async () => {
+ await act(async () => {
+ const { result, waitForNextUpdate } = renderHook(() => useTourContext(), {
+ wrapper: TourContextProvider,
+ });
+ await waitForNextUpdate();
+ const stepCount = securityTourConfig[stepId].length;
+ for (let i = 0; i < stepCount - 1; i++) {
+ result.current.incrementStep(stepId);
+ }
+ const lastStep = stepCount ? stepCount : 1;
+ expect(result.current.activeStep).toBe(lastStep);
+ result.current.incrementStep(stepId);
+ expect(result.current.activeStep).toBe(1);
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/tour.tsx b/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/tour.tsx
new file mode 100644
index 0000000000000..43f6ca15b33cb
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/tour.tsx
@@ -0,0 +1,130 @@
+/*
+ * 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 { ReactChild } from 'react';
+import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
+
+import useObservable from 'react-use/lib/useObservable';
+import { catchError, of, timeout } from 'rxjs';
+import { useLocation } from 'react-router-dom';
+import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features';
+import { isDetectionsPath } from '../../../helpers';
+import { useKibana } from '../../lib/kibana';
+import { securityTourConfig, SecurityStepId } from './tour_config';
+
+export interface TourContextValue {
+ activeStep: number;
+ endTourStep: (stepId: SecurityStepId) => void;
+ incrementStep: (stepId: SecurityStepId, step?: number) => void;
+ isTourShown: (stepId: SecurityStepId) => boolean;
+}
+
+const initialState: TourContextValue = {
+ activeStep: 0,
+ endTourStep: () => {},
+ incrementStep: () => {},
+ isTourShown: () => false,
+};
+
+const TourContext = createContext(initialState);
+
+export const RealTourContextProvider = ({ children }: { children: ReactChild }) => {
+ const { guidedOnboardingApi } = useKibana().services.guidedOnboarding;
+
+ const isRulesTourActive = useObservable(
+ guidedOnboardingApi?.isGuideStepActive$('security', SecurityStepId.rules).pipe(
+ // if no result after 30s the observable will error, but the error handler will just emit false
+ timeout(30000),
+ catchError((error) => of(false))
+ ) ?? of(false),
+ false
+ );
+ const isAlertsCasesTourActive = useObservable(
+ guidedOnboardingApi?.isGuideStepActive$('security', SecurityStepId.alertsCases).pipe(
+ // if no result after 30s the observable will error, but the error handler will just emit false
+ timeout(30000),
+ catchError((error) => of(false))
+ ) ?? of(false),
+ false
+ );
+
+ const tourStatus = useMemo(
+ () => ({
+ [SecurityStepId.rules]: isRulesTourActive,
+ [SecurityStepId.alertsCases]: isAlertsCasesTourActive,
+ }),
+ [isRulesTourActive, isAlertsCasesTourActive]
+ );
+
+ const isTourShown = useCallback((stepId: SecurityStepId) => tourStatus[stepId], [tourStatus]);
+ const [activeStep, _setActiveStep] = useState(1);
+
+ const incrementStep = useCallback((stepId: SecurityStepId) => {
+ _setActiveStep(
+ (prevState) => (prevState >= securityTourConfig[stepId].length ? 0 : prevState) + 1
+ );
+ }, []);
+
+ // TODO: @Steph figure out if we're allowing user to skip tour or not, implement this if so
+ // const onSkipTour = useCallback((stepId: SecurityStepId) => {
+ // // active state means the user is on this step but has not yet begun. so when the user hits skip,
+ // // the tour will go back to this step until they "re-start it"
+ // // guidedOnboardingApi.idkSetStepTo(stepId, 'active')
+ // }, []);
+
+ const [completeStep, setCompleteStep] = useState(null);
+
+ useEffect(() => {
+ if (!completeStep || !guidedOnboardingApi) {
+ return;
+ }
+ let ignore = false;
+ const complete = async () => {
+ await guidedOnboardingApi.completeGuideStep('security', completeStep);
+ if (!ignore) {
+ setCompleteStep(null);
+ _setActiveStep(1);
+ }
+ };
+ complete();
+ return () => {
+ ignore = true;
+ };
+ }, [completeStep, guidedOnboardingApi]);
+
+ const endTourStep = useCallback((stepId: SecurityStepId) => {
+ setCompleteStep(stepId);
+ }, []);
+
+ const context = {
+ activeStep,
+ endTourStep,
+ incrementStep,
+ isTourShown,
+ };
+
+ return {children};
+};
+
+export const TourContextProvider = ({ children }: { children: ReactChild }) => {
+ const { pathname } = useLocation();
+ const isTourEnabled = useIsExperimentalFeatureEnabled('guidedOnboarding');
+
+ if (isDetectionsPath(pathname) && isTourEnabled) {
+ return {children};
+ }
+
+ return {children};
+};
+
+export const useTourContext = (): TourContextValue => {
+ const ctx = useContext(TourContext);
+ if (!ctx) {
+ throw new Error('useTourContext can only be called inside of TourContext!');
+ }
+ return ctx;
+};
diff --git a/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/tour_config.ts b/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/tour_config.ts
new file mode 100644
index 0000000000000..f7ed05be4c418
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/tour_config.ts
@@ -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 type { EuiTourStepProps } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import type { ElementTarget } from '@elastic/eui/src/services/findElement';
+
+export const enum SecurityStepId {
+ rules = 'rules',
+ alertsCases = 'alertsCases',
+}
+
+export type StepConfig = Pick<
+ EuiTourStepProps,
+ 'step' | 'content' | 'anchorPosition' | 'title' | 'initialFocus' | 'anchor'
+> & {
+ anchor?: ElementTarget;
+ dataTestSubj: string;
+ hideNextButton?: boolean;
+ imageConfig?: {
+ altText: string;
+ src: string;
+ };
+};
+
+const defaultConfig = {
+ minWidth: 360,
+ maxWidth: 360,
+ offset: 10,
+ repositionOnScroll: true,
+};
+
+export const getTourAnchor = (step: number, stepId: SecurityStepId) =>
+ `tourStepAnchor-${stepId}-${step}`;
+
+const alertsCasesConfig: StepConfig[] = [
+ {
+ ...defaultConfig,
+ step: 1,
+ title: i18n.translate('xpack.securitySolution.guided_onboarding.tour.ruleNameStep.tourTitle', {
+ defaultMessage: 'Test alert for practice',
+ }),
+ content: i18n.translate(
+ 'xpack.securitySolution.guided_onboarding.tour.ruleNameStep.tourContent',
+ {
+ defaultMessage:
+ 'To help you practice triaging alerts, we enabled a rule to create your first alert.',
+ }
+ ),
+ anchorPosition: 'downCenter',
+ dataTestSubj: getTourAnchor(1, SecurityStepId.alertsCases),
+ initialFocus: `button[tour-step="nextButton"]`,
+ },
+ {
+ ...defaultConfig,
+ step: 2,
+ title: i18n.translate('xpack.securitySolution.guided_onboarding.tour.openFlyout.tourTitle', {
+ defaultMessage: 'Review the alert details',
+ }),
+ content: i18n.translate(
+ 'xpack.securitySolution.guided_onboarding.tour.openFlyout.tourContent',
+ {
+ defaultMessage:
+ "Some information is provided at-a-glance in the table, but for full details, you'll want to open the alert.",
+ }
+ ),
+ anchorPosition: 'rightUp',
+ dataTestSubj: getTourAnchor(2, SecurityStepId.alertsCases),
+ hideNextButton: true,
+ },
+ {
+ ...defaultConfig,
+ step: 3,
+ title: i18n.translate(
+ 'xpack.securitySolution.guided_onboarding.tour.flyoutOverview.tourTitle',
+ {
+ defaultMessage: 'Explore alert details',
+ }
+ ),
+ content: i18n.translate(
+ 'xpack.securitySolution.guided_onboarding.tour.flyoutOverview.tourContent',
+ {
+ defaultMessage:
+ 'Learn more about alerts by checking out all the information available on each tab.',
+ }
+ ),
+ // needs to use anchor to properly place tour step
+ anchor: `[tour-step="${getTourAnchor(3, SecurityStepId.alertsCases)}"] .euiTabs`,
+ anchorPosition: 'leftUp',
+ dataTestSubj: getTourAnchor(3, SecurityStepId.alertsCases),
+ },
+ {
+ ...defaultConfig,
+ step: 4,
+ title: i18n.translate('xpack.securitySolution.guided_onboarding.tour.addToCase.tourTitle', {
+ defaultMessage: 'Create a case',
+ }),
+ content: i18n.translate('xpack.securitySolution.guided_onboarding.tour.addToCase.tourContent', {
+ defaultMessage: 'From the Take action menu, add the alert to a new case.',
+ }),
+ anchorPosition: 'upRight',
+ dataTestSubj: getTourAnchor(4, SecurityStepId.alertsCases),
+ hideNextButton: true,
+ },
+ {
+ ...defaultConfig,
+ step: 5,
+ title: i18n.translate('xpack.securitySolution.guided_onboarding.tour.createCase.tourTitle', {
+ defaultMessage: `Add details`,
+ }),
+ content: i18n.translate(
+ 'xpack.securitySolution.guided_onboarding.tour.createCase.tourContent',
+ {
+ defaultMessage: `In addition to the alert, you can add any relevant information you need to the case.`,
+ }
+ ),
+ anchor: `[data-test-subj="create-case-flyout"]`,
+ anchorPosition: 'leftUp',
+ dataTestSubj: getTourAnchor(5, SecurityStepId.alertsCases),
+ hideNextButton: true,
+ },
+];
+
+interface SecurityTourConfig {
+ [SecurityStepId.rules]: StepConfig[];
+ [SecurityStepId.alertsCases]: StepConfig[];
+}
+
+export const securityTourConfig: SecurityTourConfig = {
+ /**
+ * D&R team implement your tour config here
+ */
+ [SecurityStepId.rules]: [],
+ [SecurityStepId.alertsCases]: alertsCasesConfig,
+};
diff --git a/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/tour_step.test.tsx b/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/tour_step.test.tsx
new file mode 100644
index 0000000000000..04f2cfd6a4311
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/tour_step.test.tsx
@@ -0,0 +1,225 @@
+/*
+ * 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 { GuidedOnboardingTourStep, SecurityTourStep } from './tour_step';
+import { SecurityStepId } from './tour_config';
+import { useTourContext } from './tour';
+
+jest.mock('./tour');
+const mockTourStep = jest
+ .fn()
+ .mockImplementation(({ children }: { children: React.ReactNode }) => (
+ {children}
+ ));
+jest.mock('@elastic/eui', () => {
+ const original = jest.requireActual('@elastic/eui');
+ return {
+ ...original,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ EuiTourStep: (props: any) => mockTourStep(props),
+ };
+});
+const defaultProps = {
+ isTourAnchor: true,
+ step: 1,
+ stepId: SecurityStepId.alertsCases,
+};
+
+const mockChildren = {'random child element'}
;
+
+describe('GuidedOnboardingTourStep', () => {
+ beforeEach(() => {
+ (useTourContext as jest.Mock).mockReturnValue({
+ activeStep: 1,
+ incrementStep: jest.fn(),
+ isTourShown: () => true,
+ });
+ jest.clearAllMocks();
+ });
+ it('renders as a tour step', () => {
+ const { getByTestId } = render(
+ {mockChildren}
+ );
+ const tourStep = getByTestId('tourStepMock');
+ const header = getByTestId('h1');
+ expect(tourStep).toBeInTheDocument();
+ expect(header).toBeInTheDocument();
+ });
+ it('isTourAnchor={false}, just render children', () => {
+ const { getByTestId, queryByTestId } = render(
+
+ {mockChildren}
+
+ );
+ const tourStep = queryByTestId('tourStepMock');
+ const header = getByTestId('h1');
+ expect(tourStep).not.toBeInTheDocument();
+ expect(header).toBeInTheDocument();
+ });
+});
+
+describe('SecurityTourStep', () => {
+ const { isTourAnchor: _, ...securityTourStepDefaultProps } = defaultProps;
+ beforeEach(() => {
+ (useTourContext as jest.Mock).mockReturnValue({
+ activeStep: 1,
+ incrementStep: jest.fn(),
+ isTourShown: () => true,
+ });
+ jest.clearAllMocks();
+ });
+
+ it('does not render if tour step does not exist', () => {
+ (useTourContext as jest.Mock).mockReturnValue({
+ activeStep: 99,
+ incrementStep: jest.fn(),
+ isTourShown: () => true,
+ });
+ render(
+
+ {mockChildren}
+
+ );
+ expect(mockTourStep).not.toHaveBeenCalled();
+ });
+
+ it('does not render if tour step does not equal active step', () => {
+ render(
+
+ {mockChildren}
+
+ );
+ expect(mockTourStep).not.toHaveBeenCalled();
+ });
+
+ it('does not render if security tour step is not shown', () => {
+ (useTourContext as jest.Mock).mockReturnValue({
+ activeStep: 1,
+ incrementStep: jest.fn(),
+ isTourShown: () => false,
+ });
+ render({mockChildren});
+ expect(mockTourStep).not.toHaveBeenCalled();
+ });
+
+ it('renders tour step with correct number of steppers', () => {
+ render({mockChildren});
+ const mockCall = { ...mockTourStep.mock.calls[0][0] };
+ expect(mockCall.step).toEqual(1);
+ expect(mockCall.stepsTotal).toEqual(5);
+ });
+
+ it('forces the render for step 5 of the SecurityStepId.alertsCases tour step', () => {
+ render(
+
+ {mockChildren}
+
+ );
+ const mockCall = { ...mockTourStep.mock.calls[0][0] };
+ expect(mockCall.step).toEqual(5);
+ expect(mockCall.stepsTotal).toEqual(5);
+ });
+
+ it('does render next button if step hideNextButton=false ', () => {
+ (useTourContext as jest.Mock).mockReturnValue({
+ activeStep: 3,
+ incrementStep: jest.fn(),
+ isTourShown: () => true,
+ });
+ render(
+
+ {mockChildren}
+
+ );
+ const mockCall = { ...mockTourStep.mock.calls[0][0] };
+ expect(mockCall.footerAction).toMatchInlineSnapshot(`
+
+
+
+ `);
+ });
+
+ it('if a step has an anchor declared, the tour step should be a sibling of the mockChildren', () => {
+ (useTourContext as jest.Mock).mockReturnValue({
+ activeStep: 3,
+ incrementStep: jest.fn(),
+ isTourShown: () => true,
+ });
+ const { container } = render(
+
+ {mockChildren}
+
+ );
+ const selectParent = container.querySelector(
+ `[data-test-subj="tourStepMock"] [data-test-subj="h1"]`
+ );
+ const selectSibling = container.querySelector(
+ `[data-test-subj="tourStepMock"]+[data-test-subj="h1"]`
+ );
+ expect(selectSibling).toBeInTheDocument();
+ expect(selectParent).not.toBeInTheDocument();
+ });
+
+ it('if a step does not an anchor declared, the tour step should be the parent of the mockChildren', () => {
+ (useTourContext as jest.Mock).mockReturnValue({
+ activeStep: 2,
+ incrementStep: jest.fn(),
+ isTourShown: () => true,
+ });
+ const { container } = render(
+
+ {mockChildren}
+
+ );
+ const selectParent = container.querySelector(
+ `[data-test-subj="tourStepMock"] [data-test-subj="h1"]`
+ );
+ const selectSibling = container.querySelector(
+ `[data-test-subj="tourStepMock"]+[data-test-subj="h1"]`
+ );
+ expect(selectParent).toBeInTheDocument();
+ expect(selectSibling).not.toBeInTheDocument();
+ });
+
+ it('if a tour step does not have children and has anchor, only render tour step', () => {
+ const { getByTestId } = render();
+ expect(getByTestId('tourStepMock')).toBeInTheDocument();
+ });
+
+ it('if a tour step does not have children and does not have anchor, render nothing', () => {
+ const { queryByTestId } = render(
+
+ );
+ expect(queryByTestId('tourStepMock')).not.toBeInTheDocument();
+ });
+
+ it('does not render next button if step hideNextButton=true ', () => {
+ (useTourContext as jest.Mock).mockReturnValue({
+ activeStep: 4,
+ incrementStep: jest.fn(),
+ isTourShown: () => true,
+ });
+ render(
+
+ {mockChildren}
+
+ );
+ const mockCall = { ...mockTourStep.mock.calls[0][0] };
+ expect(mockCall.footerAction).toMatchInlineSnapshot(``);
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/tour_step.tsx b/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/tour_step.tsx
new file mode 100644
index 0000000000000..ef07c5ce44a42
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/guided_onboarding_tour/tour_step.tsx
@@ -0,0 +1,116 @@
+/*
+ * 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 type { EuiTourStepProps } from '@elastic/eui';
+import { EuiButton, EuiImage, EuiSpacer, EuiText, EuiTourStep } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n-react';
+
+import { useTourContext } from './tour';
+import { securityTourConfig, SecurityStepId } from './tour_config';
+interface SecurityTourStep {
+ children?: React.ReactElement;
+ step: number;
+ stepId: SecurityStepId;
+}
+
+export const SecurityTourStep = ({ children, step, stepId }: SecurityTourStep) => {
+ const { activeStep, incrementStep, isTourShown } = useTourContext();
+ const tourStep = useMemo(
+ () => securityTourConfig[stepId].find((config) => config.step === step),
+ [step, stepId]
+ );
+ const onClick = useCallback(() => incrementStep(stepId), [incrementStep, stepId]);
+ // step === 5 && stepId === SecurityStepId.alertsCases is in Cases app and out of context.
+ // If we mount this step, we know we need to render it
+ // we are also managing the context on the siem end in the background
+ const overrideContext = step === 5 && stepId === SecurityStepId.alertsCases;
+ if (tourStep == null || ((step !== activeStep || !isTourShown(stepId)) && !overrideContext)) {
+ return children ? children : null;
+ }
+
+ const { anchor, content, imageConfig, dataTestSubj, hideNextButton = false, ...rest } = tourStep;
+
+ const footerAction: EuiTourStepProps['footerAction'] = !hideNextButton ? (
+
+
+
+ ) : (
+ <>
+ {/* Passing empty element instead of undefined. If undefined "Skip tour" button is shown, we do not want that*/}
+ >
+ );
+
+ const commonProps = {
+ ...rest,
+ content: (
+ <>
+
+ {content}
+
+ {imageConfig && (
+ <>
+
+
+ >
+ )}
+ >
+ ),
+ footerAction,
+ // we would not have mounted this component if it was not open
+ isStepOpen: true,
+ // guided onboarding does not allow skipping tour through the steps
+ onFinish: () => null,
+ stepsTotal: securityTourConfig[stepId].length,
+ // TODO: re-add panelProps
+ // EUI has a bug https://github.com/elastic/eui/issues/6297
+ // where any panelProps overwrite their panelProps,
+ // so we lose cool things like the EuiBeacon
+ // panelProps: {
+ // 'data-test-subj': dataTestSubj,
+ // }
+ };
+
+ // tour step either needs children or an anchor element
+ // see type EuiTourStepAnchorProps
+ return anchor != null ? (
+ <>
+
+ <>{children}>
+ >
+ ) : children != null ? (
+ {children}
+ ) : null;
+};
+
+interface GuidedOnboardingTourStep extends SecurityTourStep {
+ // can be false if the anchor is an iterative element
+ // do not use this as an "is tour active" check, the SecurityTourStep checks that anyway
+ isTourAnchor?: boolean;
+}
+
+// wraps tour anchor component
+// and gives the tour step itself a place to mount once it is active
+// mounts the tour step with a delay to ensure the anchor will render first
+export const GuidedOnboardingTourStep = ({
+ children,
+ // can be false if the anchor is an iterative element
+ // do not use this as an "is tour active" check, the SecurityTourStep checks that anyway
+ isTourAnchor = true,
+ ...props
+}: GuidedOnboardingTourStep) =>
+ isTourAnchor ? {children} : <>{children}>;
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx
index 5a99df01e5328..1d13d100b4d88 100644
--- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx
@@ -16,7 +16,7 @@ import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental
import { TestProviders } from '../../../mock';
import { CASES_FEATURE_ID } from '../../../../../common/constants';
import { useCanSeeHostIsolationExceptionsMenu } from '../../../../management/pages/host_isolation_exceptions/view/hooks';
-import { useTourContext } from '../../guided_onboarding';
+import { useTourContext } from '../../guided_onboarding_tour';
import { useUserPrivileges } from '../../user_privileges';
import {
noCasesPermissions,
@@ -38,7 +38,7 @@ jest.mock('../../../hooks/use_selector');
jest.mock('../../../hooks/use_experimental_features');
jest.mock('../../../utils/route/use_route_spy');
jest.mock('../../../../management/pages/host_isolation_exceptions/view/hooks');
-jest.mock('../../guided_onboarding');
+jest.mock('../../guided_onboarding_tour');
jest.mock('../../user_privileges');
const mockUseUserPrivileges = useUserPrivileges as jest.Mock;
@@ -187,25 +187,4 @@ describe('useSecuritySolutionNavigation', () => {
});
});
});
-
- describe('Guided onboarding tour', () => {
- it('nav can be collapsed if tour is not shown', () => {
- const { result } = renderHook<{}, KibanaPageTemplateProps['solutionNav']>(
- () => useSecuritySolutionNavigation(),
- { wrapper: TestProviders }
- );
-
- expect(result.current?.canBeCollapsed).toBe(true);
- });
- it(`nav can't be collapsed if tour is shown`, () => {
- (useTourContext as jest.Mock).mockReturnValue({ isTourShown: true });
-
- const { result } = renderHook<{}, KibanaPageTemplateProps['solutionNav']>(
- () => useSecuritySolutionNavigation(),
- { wrapper: TestProviders }
- );
-
- expect(result.current?.canBeCollapsed).toBe(false);
- });
- });
});
diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_primary_navigation.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_primary_navigation.tsx
index 9e83ae9339dcd..647193357b66b 100644
--- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_primary_navigation.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_primary_navigation.tsx
@@ -13,7 +13,6 @@ import type { PrimaryNavigationProps } from './types';
import { usePrimaryNavigationItems } from './use_navigation_items';
import { useIsGroupedNavigationEnabled } from '../helpers';
import { SecuritySideNav } from '../security_side_nav';
-import { useTourContext } from '../../guided_onboarding';
const translatedNavTitle = i18n.translate('xpack.securitySolution.navigation.mainLabel', {
defaultMessage: 'Security',
@@ -31,8 +30,6 @@ export const usePrimaryNavigation = ({
const [selectedTabId, setSelectedTabId] = useState(mapLocationToTab());
- const { isTourShown } = useTourContext();
-
useEffect(() => {
const currentTabSelected = mapLocationToTab();
@@ -49,7 +46,7 @@ export const usePrimaryNavigation = ({
});
return {
- canBeCollapsed: !isTourShown,
+ canBeCollapsed: true,
name: translatedNavTitle,
icon: 'logoSecurity',
...(isGroupedNavigationEnabled
diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts
index 5d2fed9fc6241..efa9ce4831be7 100644
--- a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts
+++ b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts
@@ -44,6 +44,7 @@ import { noCasesPermissions } from '../../../cases_test_utils';
import { triggersActionsUiMock } from '@kbn/triggers-actions-ui-plugin/public/mocks';
import { mockApm } from '../apm/service.mock';
import { cloudExperimentsMock } from '@kbn/cloud-experiments-plugin/common/mocks';
+import { guidedOnboardingMock } from '@kbn/guided-onboarding-plugin/public/mocks';
const mockUiSettings: Record = {
[DEFAULT_TIME_RANGE]: { from: 'now-15m', to: 'now', mode: 'quick' },
@@ -106,6 +107,7 @@ export const createStartServicesMock = (
cases.helpers.getUICapabilities.mockReturnValue(noCasesPermissions());
const triggersActionsUi = triggersActionsUiMock.createStart();
const cloudExperiments = cloudExperimentsMock.createStartMock();
+ const guidedOnboarding = guidedOnboardingMock.createStart();
return {
...core,
@@ -173,6 +175,7 @@ export const createStartServicesMock = (
},
triggersActionsUi,
cloudExperiments,
+ guidedOnboarding,
} as unknown as StartServices;
};
diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx
index 538ae5b4ba211..70455fa342ab5 100644
--- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_to_case_actions.tsx
@@ -9,6 +9,9 @@ import React, { useCallback, useMemo } from 'react';
import { EuiContextMenuItem } from '@elastic/eui';
import { CommentType } from '@kbn/cases-plugin/common';
import type { CaseAttachmentsWithoutOwner } from '@kbn/cases-plugin/public';
+import { GuidedOnboardingTourStep } from '../../../../common/components/guided_onboarding_tour/tour_step';
+import { SecurityStepId } from '../../../../common/components/guided_onboarding_tour/tour_config';
+import { useTourContext } from '../../../../common/components/guided_onboarding_tour';
import { useGetUserCasesPermissions, useKibana } from '../../../../common/lib/kibana';
import type { TimelineNonEcsData } from '../../../../../common/search_strategy';
import type { Ecs } from '../../../../../common/ecs';
@@ -53,9 +56,18 @@ export const useAddToCaseActions = ({
: [];
}, [casesUi.helpers, ecsData, nonEcsData]);
+ const { activeStep, endTourStep, incrementStep, isTourShown } = useTourContext();
+
+ const afterCaseCreated = useCallback(async () => {
+ if (isTourShown(SecurityStepId.alertsCases)) {
+ endTourStep(SecurityStepId.alertsCases);
+ }
+ }, [endTourStep, isTourShown]);
+
const createCaseFlyout = casesUi.hooks.getUseCasesAddToNewCaseFlyout({
onClose: onMenuItemClick,
onSuccess,
+ afterCaseCreated,
});
const selectCaseModal = casesUi.hooks.getUseCasesAddToExistingCaseModal({
@@ -66,8 +78,22 @@ export const useAddToCaseActions = ({
const handleAddToNewCaseClick = useCallback(() => {
// TODO rename this, this is really `closePopover()`
onMenuItemClick();
- createCaseFlyout.open({ attachments: caseAttachments });
- }, [onMenuItemClick, createCaseFlyout, caseAttachments]);
+ createCaseFlyout.open({
+ attachments: caseAttachments,
+ ...(isTourShown(SecurityStepId.alertsCases) && activeStep === 4
+ ? {
+ headerContent: (
+ // isTourAnchor=true no matter what in order to
+ // force active guide step outside of security solution (cases)
+
+ ),
+ }
+ : {}),
+ });
+ if (isTourShown(SecurityStepId.alertsCases) && activeStep === 4) {
+ incrementStep(SecurityStepId.alertsCases);
+ }
+ }, [onMenuItemClick, createCaseFlyout, caseAttachments, isTourShown, activeStep, incrementStep]);
const handleAddToExistingCaseClick = useCallback(() => {
// TODO rename this, this is really `closePopover()`
diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx
index f4364443a6dfa..f9d5a5bc998c0 100644
--- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx
@@ -8,6 +8,8 @@
import React, { useCallback, useMemo, useState } from 'react';
import { EuiButton, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
import type { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
+import { GuidedOnboardingTourStep } from '../../../common/components/guided_onboarding_tour/tour_step';
+import { SecurityStepId } from '../../../common/components/guided_onboarding_tour/tour_config';
import { isActiveTimeline } from '../../../helpers';
import { TableId } from '../../../../common/types';
import { useResponderActionItem } from '../endpoint_responder';
@@ -252,19 +254,24 @@ export const TakeActionDropdown = React.memo(
]
);
- const takeActionButton = useMemo(() => {
- return (
-
- {TAKE_ACTION}
-
- );
- }, [togglePopoverHandler]);
+ const takeActionButton = useMemo(
+ () => (
+
+
+ {TAKE_ACTION}
+
+
+ ),
+
+ [togglePopoverHandler]
+ );
+
return items.length && !loadingEventDetails && ecsData ? (
= ({
- browserFields,
- columnId,
- data,
- ecsData,
- eventId,
- globalFilters,
- header,
- isDetails,
- isDraggable,
- isExpandable,
- isExpanded,
- linkValues,
- rowIndex,
- colIndex,
- rowRenderers,
- setCellProps,
- scopeId,
- truncate,
-}) => (
-
-);
+export const RenderCellValue: React.FC = (
+ props
+) => {
+ const { columnId, rowIndex, scopeId } = props;
+ const isTourAnchor = useMemo(
+ () =>
+ columnId === SIGNAL_RULE_NAME_FIELD_NAME &&
+ isDetectionsAlertsTable(scopeId) &&
+ rowIndex === 0,
+ [columnId, rowIndex, scopeId]
+ );
+
+ return (
+
+
+
+ );
+};
export const useRenderCellValue = ({
setFlyoutAlert,
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx
index 2c1b5b06a284e..a0384d7707534 100644
--- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx
+++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx
@@ -12,6 +12,10 @@ import { noop } from 'lodash/fp';
import styled from 'styled-components';
import { DEFAULT_ACTION_BUTTON_WIDTH } from '@kbn/timelines-plugin/public';
+import { GuidedOnboardingTourStep } from '../../../../../common/components/guided_onboarding_tour/tour_step';
+import { isDetectionsAlertsTable } from '../../../../../common/components/top_n/helpers';
+import { useTourContext } from '../../../../../common/components/guided_onboarding_tour';
+import { SecurityStepId } from '../../../../../common/components/guided_onboarding_tour/tour_config';
import { getScopedActions, isTimelineScope } from '../../../../../helpers';
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
import { eventHasNotes, getEventType, getPinOnClick } from '../helpers';
@@ -201,6 +205,24 @@ const ActionsComponent: React.FC = ({
scopedActions,
]);
+ const { isTourShown, incrementStep } = useTourContext();
+
+ const isTourAnchor = useMemo(
+ () =>
+ isTourShown(SecurityStepId.alertsCases) &&
+ eventType === 'signal' &&
+ isDetectionsAlertsTable(timelineId) &&
+ ariaRowindex === 1,
+ [isTourShown, ariaRowindex, eventType, timelineId]
+ );
+
+ const onExpandEvent = useCallback(() => {
+ if (isTourAnchor) {
+ incrementStep(SecurityStepId.alertsCases);
+ }
+ onEventDetailsPanelOpened();
+ }, [incrementStep, isTourAnchor, onEventDetailsPanelOpened]);
+
return (
{showCheckboxes && !tGridEnabled && (
@@ -220,19 +242,25 @@ const ActionsComponent: React.FC = ({
)}
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
<>
{timelineId !== TimelineId.active && (
Promise>(asyncNoop);
const abortCtrl = useRef(new AbortController());
const searchSubscription$ = useRef(new Subscription());
- const [loading, setLoading] = useState(false);
+
+ // loading = false initial state causes flashes of empty tables
+ const [loading, setLoading] = useState(true);
const [timelineDetailsRequest, setTimelineDetailsRequest] =
useState(null);
const { addError, addWarning } = useAppToasts();
diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts
index a5f8e5897230d..70a5de2c00af6 100644
--- a/x-pack/plugins/security_solution/public/types.ts
+++ b/x-pack/plugins/security_solution/public/types.ts
@@ -41,6 +41,7 @@ import type {
} from '@kbn/saved-objects-tagging-oss-plugin/public';
import type { ThreatIntelligencePluginStart } from '@kbn/threat-intelligence-plugin/public';
import type { CloudExperimentsPluginStart } from '@kbn/cloud-experiments-plugin/common';
+import type { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/public';
import type { ResolverPluginSetup } from './resolver/types';
import type { Inspect } from '../common/search_strategy';
import type { Detections } from './detections';
@@ -76,6 +77,7 @@ export interface StartPlugins {
embeddable: EmbeddableStart;
inspector: InspectorStart;
fleet?: FleetStart;
+ guidedOnboarding: GuidedOnboardingPluginStart;
kubernetesSecurity: KubernetesSecurityStart;
lens: LensPublicStart;
lists?: ListsPluginStart;
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index 7beb42271c73f..3db06e4c80d2e 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -28235,21 +28235,7 @@
"xpack.securitySolution.getCurrentUser.unknownUser": "Inconnu",
"xpack.securitySolution.globalHeader.buttonAddData": "Ajouter des intégrations",
"xpack.securitySolution.goToDocumentationButton": "Afficher la documentation",
- "xpack.securitySolution.guided_onboarding.endTour.buttonLabel": "Terminer la visite",
"xpack.securitySolution.guided_onboarding.nextStep.buttonLabel": "Suivant",
- "xpack.securitySolution.guided_onboarding.skipTour.buttonLabel": "Ignorer la visite",
- "xpack.securitySolution.guided_onboarding.tour.alertsStep.imageAltText": "Démonstration des alertes",
- "xpack.securitySolution.guided_onboarding.tour.alertsStep.tourContent": "Sachez quand les conditions d'une règle sont remplies, afin de pouvoir commencer votre investigation immédiatement. Configurez des notifications avec des plateformes tierces telles que Slack, PagerDuty et ServiceNow.",
- "xpack.securitySolution.guided_onboarding.tour.alertsStep.tourTitle": "Soyez informé en cas de modification",
- "xpack.securitySolution.guided_onboarding.tour.casesStep.imageAltText": "Démonstration des cas",
- "xpack.securitySolution.guided_onboarding.tour.casesStep.tourContent": "Recueillez des éléments probants, ajoutez des collaborateurs et transmettez même les détails de l'affaire à des systèmes tiers de gestion des cas.",
- "xpack.securitySolution.guided_onboarding.tour.casesStep.tourTitle": "Créez un cas pour suivre votre investigation",
- "xpack.securitySolution.guided_onboarding.tour.dataStep.tourContent": "Recueillez des données à partir de vos points de terminaison en utilisant l'agent Elastic et une variété d'intégrations tierces.",
- "xpack.securitySolution.guided_onboarding.tour.dataStep.tourTitle": "Commencez à collecter vos données !",
- "xpack.securitySolution.guided_onboarding.tour.manageStep.tourContent": "Décidez de ce qui est important pour vous et votre environnement, et créez des règles pour détecter et prévenir les activités malveillantes. ",
- "xpack.securitySolution.guided_onboarding.tour.manageStep.tourTitle": "Protégez votre écosystème",
- "xpack.securitySolution.guided_onboarding.tour.overviewStep.tourContent": "Faites une visite rapide pour découvrir un flux de travail unifié pour enquêter sur les activités suspectes.",
- "xpack.securitySolution.guided_onboarding.tour.overviewStep.tourTitle": "Bienvenue dans Elastic Security",
"xpack.securitySolution.handleInputAreaState.inputPlaceholderText": "Soumettre l’action de réponse",
"xpack.securitySolution.header.editableTitle.cancel": "Annuler",
"xpack.securitySolution.header.editableTitle.save": "Enregistrer",
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index fcc2c1c49cdf8..b825ccd3f77c6 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -28210,21 +28210,7 @@
"xpack.securitySolution.getCurrentUser.unknownUser": "不明",
"xpack.securitySolution.globalHeader.buttonAddData": "統合の追加",
"xpack.securitySolution.goToDocumentationButton": "ドキュメンテーションを表示",
- "xpack.securitySolution.guided_onboarding.endTour.buttonLabel": "ツアーを終了",
"xpack.securitySolution.guided_onboarding.nextStep.buttonLabel": "次へ",
- "xpack.securitySolution.guided_onboarding.skipTour.buttonLabel": "ツアーをスキップ",
- "xpack.securitySolution.guided_onboarding.tour.alertsStep.imageAltText": "アラートデモ",
- "xpack.securitySolution.guided_onboarding.tour.alertsStep.tourContent": "ルールの条件が満たされているときを把握し、調査をすぐに開始できるようにします。Slack、PagerDuty、ServiceNowなどのサードパーティプラットフォームで通知を設定します。",
- "xpack.securitySolution.guided_onboarding.tour.alertsStep.tourTitle": "変更が発生したときに通知",
- "xpack.securitySolution.guided_onboarding.tour.casesStep.imageAltText": "ケースのデモ",
- "xpack.securitySolution.guided_onboarding.tour.casesStep.tourContent": "エビデンスを収集し、その他のコラボレーターを追加し、さらにケース詳細をサードパーティケース管理システムにプッシュします。",
- "xpack.securitySolution.guided_onboarding.tour.casesStep.tourTitle": "調査を追跡するには、ケースを作成します",
- "xpack.securitySolution.guided_onboarding.tour.dataStep.tourContent": "Elasticエージェントとさまざまなサードパーティ統合を使用して、エンドポイントからデータを収集します。",
- "xpack.securitySolution.guided_onboarding.tour.dataStep.tourTitle": "データの収集を開始してください。",
- "xpack.securitySolution.guided_onboarding.tour.manageStep.tourContent": "重要な項目と環境を決定し、悪意のあるアクティビティを検出および防御するルールを作成します。",
- "xpack.securitySolution.guided_onboarding.tour.manageStep.tourTitle": "エコシステムを保護",
- "xpack.securitySolution.guided_onboarding.tour.overviewStep.tourContent": "不審なアクティビティの調査については、統合ワークフローを説明するクイックガイドを表示してください。",
- "xpack.securitySolution.guided_onboarding.tour.overviewStep.tourTitle": "Elastic Securityへようこそ",
"xpack.securitySolution.handleInputAreaState.inputPlaceholderText": "対応アクションを送信",
"xpack.securitySolution.header.editableTitle.cancel": "キャンセル",
"xpack.securitySolution.header.editableTitle.save": "保存",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index ff49a2cd73d6c..9bee281887e34 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -28244,21 +28244,7 @@
"xpack.securitySolution.getCurrentUser.unknownUser": "未知",
"xpack.securitySolution.globalHeader.buttonAddData": "添加集成",
"xpack.securitySolution.goToDocumentationButton": "查看文档",
- "xpack.securitySolution.guided_onboarding.endTour.buttonLabel": "结束教程",
"xpack.securitySolution.guided_onboarding.nextStep.buttonLabel": "下一步",
- "xpack.securitySolution.guided_onboarding.skipTour.buttonLabel": "跳过教程",
- "xpack.securitySolution.guided_onboarding.tour.alertsStep.imageAltText": "告警演示",
- "xpack.securitySolution.guided_onboarding.tour.alertsStep.tourContent": "知道何时满足规则条件,以便您立即开始调查。通过 Slack、PagerDuty 和 ServiceNow 等第三方平台设置通知。",
- "xpack.securitySolution.guided_onboarding.tour.alertsStep.tourTitle": "发生更改时接收通知",
- "xpack.securitySolution.guided_onboarding.tour.casesStep.imageAltText": "案例演示",
- "xpack.securitySolution.guided_onboarding.tour.casesStep.tourContent": "收集证据,添加更多协作者,甚至将案例详情推送到第三方案例管理系统。",
- "xpack.securitySolution.guided_onboarding.tour.casesStep.tourTitle": "创建案例以跟踪您的调查",
- "xpack.securitySolution.guided_onboarding.tour.dataStep.tourContent": "使用 Elastic 代理和一系列第三方集成从您的终端收集数据。",
- "xpack.securitySolution.guided_onboarding.tour.dataStep.tourTitle": "开始收集您的数据!",
- "xpack.securitySolution.guided_onboarding.tour.manageStep.tourContent": "确定对您和您的环境至关重要的事项,并创建规则来检测并防止恶意活动。",
- "xpack.securitySolution.guided_onboarding.tour.manageStep.tourTitle": "保护您的生态系统",
- "xpack.securitySolution.guided_onboarding.tour.overviewStep.tourContent": "学习快速教程以浏览调查可疑活动的统一工作流。",
- "xpack.securitySolution.guided_onboarding.tour.overviewStep.tourTitle": "欢迎使用 Elastic Security",
"xpack.securitySolution.handleInputAreaState.inputPlaceholderText": "提交响应操作",
"xpack.securitySolution.header.editableTitle.cancel": "取消",
"xpack.securitySolution.header.editableTitle.save": "保存",