mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
* [Guided onboarding] Security tour: Fixed localStorage value parsing, also "add integrations" closes the tour
* [Guided onboarding] Security tour: Cypress tests
* [Guided onboarding] Security tour: Update text copy
* [Guided onboarding] Security tour: Added useCallback
(cherry picked from commit dcc835ec51
)
Co-authored-by: Yulia Čech <6585477+yuliacech@users.noreply.github.com>
This commit is contained in:
parent
114d1b8e10
commit
3fd7d9e7b9
10 changed files with 233 additions and 34 deletions
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { 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,
|
||||
MANAGE_STEP,
|
||||
ALERTS_STEP,
|
||||
CASES_STEP,
|
||||
DATA_STEP,
|
||||
} from '../../screens/guided_onboarding';
|
||||
|
||||
before(() => {
|
||||
login();
|
||||
});
|
||||
|
||||
describe('Guided onboarding tour', () => {
|
||||
describe('Tour is enabled', () => {
|
||||
beforeEach(() => {
|
||||
visit(OVERVIEW_URL);
|
||||
window.localStorage.setItem(SECURITY_TOUR_ACTIVE_KEY, 'true');
|
||||
});
|
||||
|
||||
it('can be completed', () => {
|
||||
// Step 1: Overview
|
||||
cy.get(WELCOME_STEP).should('be.visible');
|
||||
goToNextStep(WELCOME_STEP);
|
||||
|
||||
// Step 2: Manage
|
||||
cy.get(MANAGE_STEP).should('be.visible');
|
||||
goToNextStep(MANAGE_STEP);
|
||||
|
||||
// Step 3: Alerts
|
||||
cy.get(ALERTS_STEP).should('be.visible');
|
||||
goToNextStep(ALERTS_STEP);
|
||||
|
||||
// Step 4: Cases
|
||||
cy.get(CASES_STEP).should('be.visible');
|
||||
goToNextStep(CASES_STEP);
|
||||
|
||||
// Step 5: Add data
|
||||
cy.get(DATA_STEP).should('be.visible');
|
||||
completeTour();
|
||||
});
|
||||
|
||||
it('can be skipped', () => {
|
||||
cy.get(WELCOME_STEP).should('be.visible');
|
||||
|
||||
skipTour();
|
||||
// step 1 is not displayed
|
||||
cy.get(WELCOME_STEP).should('not.exist');
|
||||
// step 2 is not displayed
|
||||
cy.get(MANAGE_STEP).should('not.exist');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -34,6 +34,10 @@ module.exports = (on) => {
|
|||
loader: 'ts-loader',
|
||||
options: { transpileOnly: true },
|
||||
},
|
||||
{
|
||||
test: /\.gif$/,
|
||||
loader: 'file-loader',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const WELCOME_STEP = '[data-test-subj="welcomeStep"]';
|
||||
export const MANAGE_STEP = '[data-test-subj="manageStep"]';
|
||||
export const ALERTS_STEP = '[data-test-subj="alertsStep"]';
|
||||
export const CASES_STEP = '[data-test-subj="casesStep"]';
|
||||
export const DATA_STEP = '[data-test-subj="dataStep"]';
|
||||
|
||||
export const NEXT_STEP_BUTTON = '[data-test-subj="onboarding--securityTourNextStepButton"]';
|
||||
export const END_TOUR_BUTTON = '[data-test-subj="onboarding--securityTourEndButton"]';
|
||||
export const SKIP_TOUR_BUTTON = '[data-test-subj="onboarding--securityTourSkipButton"]';
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
NEXT_STEP_BUTTON,
|
||||
END_TOUR_BUTTON,
|
||||
DATA_STEP,
|
||||
SKIP_TOUR_BUTTON,
|
||||
} from '../screens/guided_onboarding';
|
||||
|
||||
export const goToNextStep = (currentStep: string) => {
|
||||
cy.get(`${currentStep} ${NEXT_STEP_BUTTON}`).click();
|
||||
};
|
||||
|
||||
export const completeTour = () => {
|
||||
cy.get(`${DATA_STEP} ${END_TOUR_BUTTON}`).click();
|
||||
};
|
||||
|
||||
export const skipTour = () => {
|
||||
cy.get(SKIP_TOUR_BUTTON).click();
|
||||
};
|
|
@ -10,7 +10,7 @@ import {
|
|||
EuiHeaderSection,
|
||||
EuiHeaderSectionItem,
|
||||
} from '@elastic/eui';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { createPortalNode, InPortal, OutPortal } from 'react-reverse-portal';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -27,6 +27,7 @@ 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',
|
||||
|
@ -69,6 +70,12 @@ export const GlobalHeader = React.memo(
|
|||
};
|
||||
}, [portalNode, setHeaderActionMenu, theme.theme$]);
|
||||
|
||||
const { isTourShown, endTour } = useTourContext();
|
||||
const closeOnboardingTourIfShown = useCallback(() => {
|
||||
if (isTourShown) {
|
||||
endTour();
|
||||
}
|
||||
}, [isTourShown, endTour]);
|
||||
return (
|
||||
<InPortal node={portalNode}>
|
||||
<EuiHeaderSection side="right">
|
||||
|
@ -85,6 +92,7 @@ export const GlobalHeader = React.memo(
|
|||
data-test-subj="add-data"
|
||||
href={href}
|
||||
iconType="indexOpen"
|
||||
onClick={closeOnboardingTourIfShown}
|
||||
>
|
||||
{BUTTON_ADD_DATA}
|
||||
</EuiHeaderLink>
|
||||
|
|
|
@ -55,15 +55,17 @@ const HomePageComponent: React.FC<HomePageProps> = ({
|
|||
return (
|
||||
<SecuritySolutionAppWrapper className="kbnAppWrapper">
|
||||
<ConsoleManager>
|
||||
<GlobalHeader setHeaderActionMenu={setHeaderActionMenu} />
|
||||
<DragDropContextWrapper browserFields={browserFields}>
|
||||
<TourContextProvider>
|
||||
<SecuritySolutionTemplateWrapper onAppLeave={onAppLeave}>
|
||||
{children}
|
||||
</SecuritySolutionTemplateWrapper>
|
||||
</TourContextProvider>
|
||||
</DragDropContextWrapper>
|
||||
<HelpMenu />
|
||||
<TourContextProvider>
|
||||
<>
|
||||
<GlobalHeader setHeaderActionMenu={setHeaderActionMenu} />
|
||||
<DragDropContextWrapper browserFields={browserFields}>
|
||||
<SecuritySolutionTemplateWrapper onAppLeave={onAppLeave}>
|
||||
{children}
|
||||
</SecuritySolutionTemplateWrapper>
|
||||
</DragDropContextWrapper>
|
||||
<HelpMenu />
|
||||
</>
|
||||
</TourContextProvider>
|
||||
</ConsoleManager>
|
||||
</SecuritySolutionAppWrapper>
|
||||
);
|
||||
|
|
|
@ -5,4 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { useTourContext, TourContextProvider } from './tour';
|
||||
export {
|
||||
useTourContext,
|
||||
TourContextProvider,
|
||||
SECURITY_TOUR_ACTIVE_KEY,
|
||||
SECURITY_TOUR_STEP_KEY,
|
||||
} from './tour';
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -28,13 +28,14 @@ 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 => {
|
||||
return Boolean(localStorage.getItem(SECURITY_TOUR_ACTIVE_KEY));
|
||||
const localStorageValue = localStorage.getItem(SECURITY_TOUR_ACTIVE_KEY);
|
||||
return localStorageValue ? JSON.parse(localStorageValue) : false;
|
||||
};
|
||||
const saveIsTourActiveToLocalStorage = (isTourActive: boolean): void => {
|
||||
export const saveIsTourActiveToLocalStorage = (isTourActive: boolean): void => {
|
||||
localStorage.setItem(SECURITY_TOUR_ACTIVE_KEY, JSON.stringify(isTourActive));
|
||||
};
|
||||
|
||||
const getTourStepFromLocalStorage = (): number => {
|
||||
export const getTourStepFromLocalStorage = (): number => {
|
||||
return Number(localStorage.getItem(SECURITY_TOUR_STEP_KEY) ?? 1);
|
||||
};
|
||||
const saveTourStepToLocalStorage = (step: number): void => {
|
||||
|
@ -96,7 +97,7 @@ const getSteps = (tourControls: {
|
|||
</EuiButtonEmpty>
|
||||
);
|
||||
return tourConfig.map((stepConfig: StepConfig) => {
|
||||
const { content, imageConfig, ...rest } = stepConfig;
|
||||
const { content, imageConfig, dataTestSubj, ...rest } = stepConfig;
|
||||
return (
|
||||
<EuiTourStep
|
||||
{...rest}
|
||||
|
@ -107,6 +108,9 @@ const getSteps = (tourControls: {
|
|||
stepsTotal={tourConfig.length}
|
||||
isStepOpen={stepConfig.step === activeStep}
|
||||
onFinish={() => resetTour()}
|
||||
panelProps={{
|
||||
'data-test-subj': dataTestSubj,
|
||||
}}
|
||||
content={
|
||||
<>
|
||||
<EuiText size="xs">
|
||||
|
@ -128,10 +132,12 @@ const getSteps = (tourControls: {
|
|||
|
||||
export interface TourContextValue {
|
||||
isTourShown: boolean;
|
||||
endTour: () => void;
|
||||
}
|
||||
|
||||
const TourContext = createContext<TourContextValue>({
|
||||
isTourShown: false,
|
||||
endTour: () => {},
|
||||
} as TourContextValue);
|
||||
|
||||
export const TourContextProvider = ({ children }: { children: ReactChild }) => {
|
||||
|
@ -163,7 +169,7 @@ export const TourContextProvider = ({ children }: { children: ReactChild }) => {
|
|||
|
||||
const isSmallScreen = useIsWithinBreakpoints(['xs', 's']);
|
||||
const showTour = isTourActive && !isSmallScreen;
|
||||
const context: TourContextValue = { isTourShown: showTour };
|
||||
const context: TourContextValue = { isTourShown: showTour, endTour: resetTour };
|
||||
return (
|
||||
<TourContext.Provider value={context}>
|
||||
<>
|
||||
|
|
|
@ -10,11 +10,9 @@ 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<
|
||||
EuiTourStepProps,
|
||||
'step' | 'content' | 'anchorPosition' | 'title' | 'data-test-subj'
|
||||
> & {
|
||||
export type StepConfig = Pick<EuiTourStepProps, 'step' | 'content' | 'anchorPosition' | 'title'> & {
|
||||
anchor: string;
|
||||
dataTestSubj: string;
|
||||
imageConfig?: {
|
||||
altText: string;
|
||||
src: string;
|
||||
|
@ -33,38 +31,39 @@ export const tourConfig: TourConfig = [
|
|||
'xpack.securitySolution.guided_onboarding.tour.overviewStep.tourContent',
|
||||
{
|
||||
defaultMessage:
|
||||
'Take a quick tour of the Security solution to get a feel for how it works.',
|
||||
'Take a quick tour to explore a unified workflow for investigating suspicious activity.',
|
||||
}
|
||||
),
|
||||
anchor: `[id^="SolutionNav"]`,
|
||||
anchorPosition: 'rightUp',
|
||||
'data-test-subj': 'welcomeStep',
|
||||
dataTestSubj: 'welcomeStep',
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
title: i18n.translate('xpack.securitySolution.guided_onboarding.tour.manageStep.tourTitle', {
|
||||
defaultMessage: 'Define prevention, detection, and response across your entire ecosystem',
|
||||
defaultMessage: 'Protect your ecosystem',
|
||||
}),
|
||||
content: i18n.translate(
|
||||
'xpack.securitySolution.guided_onboarding.tour.manageStep.tourContent',
|
||||
{
|
||||
defaultMessage:
|
||||
'Create rules to detect and prevent malicious activity, and implement threat intelligence to protect endpoints and cloud workloads.',
|
||||
'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',
|
||||
'data-test-subj': 'manageStep',
|
||||
dataTestSubj: 'manageStep',
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
title: i18n.translate('xpack.securitySolution.guided_onboarding.tour.alertsStep.tourTitle', {
|
||||
defaultMessage: 'Get notified when your security rules are triggered',
|
||||
defaultMessage: 'Get notified when something changes',
|
||||
}),
|
||||
content: i18n.translate(
|
||||
'xpack.securitySolution.guided_onboarding.tour.alertsStep.tourContent',
|
||||
{
|
||||
defaultMessage: 'Detect, investigate, and respond to evolving threats in your environment.',
|
||||
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"]`,
|
||||
|
@ -78,16 +77,16 @@ export const tourConfig: TourConfig = [
|
|||
}
|
||||
),
|
||||
},
|
||||
'data-test-subj': 'alertsStep',
|
||||
dataTestSubj: 'alertsStep',
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
title: i18n.translate('xpack.securitySolution.guided_onboarding.tour.casesStep.tourTitle', {
|
||||
defaultMessage: 'Collect and share information about security issues',
|
||||
defaultMessage: 'Create a case to track your investigation',
|
||||
}),
|
||||
content: i18n.translate('xpack.securitySolution.guided_onboarding.tour.casesStep.tourContent', {
|
||||
defaultMessage:
|
||||
'Track key investigation details, collect alerts in a central location, and more.',
|
||||
'Collect evidence, add more collaborators, and even push case details to third-party case management systems.',
|
||||
}),
|
||||
anchor: `[data-test-subj="groupedNavItemLink-cases"]`,
|
||||
anchorPosition: 'rightUp',
|
||||
|
@ -100,18 +99,18 @@ export const tourConfig: TourConfig = [
|
|||
}
|
||||
),
|
||||
},
|
||||
'data-test-subj': 'casesStep',
|
||||
dataTestSubj: 'casesStep',
|
||||
},
|
||||
{
|
||||
step: 5,
|
||||
title: i18n.translate('xpack.securitySolution.guided_onboarding.tour.dataStep.tourTitle', {
|
||||
defaultMessage: `You're ready!`,
|
||||
defaultMessage: `Start gathering your data!`,
|
||||
}),
|
||||
content: i18n.translate('xpack.securitySolution.guided_onboarding.tour.dataStep.tourContent', {
|
||||
defaultMessage: `View and add your first integration to start protecting your environment. Return to the Security solution when you're done.`,
|
||||
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',
|
||||
'data-test-subj': 'dataStep',
|
||||
dataTestSubj: 'dataStep',
|
||||
},
|
||||
];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue