From 8969009e3b105fcfafb8c5fa6a68ee15eb8d6972 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Tue, 1 Nov 2022 09:59:52 -0600 Subject: [PATCH] [Security solution] Guided onboarding unhappy path fixes (#144178) --- .../guided_onboarding_tour/README.md | 4 +- .../guided_onboarding_tour/tour.tsx | 2 +- .../guided_onboarding_tour/tour_config.ts | 37 +++-- .../guided_onboarding_tour/tour_step.test.tsx | 79 +++++++++-- .../guided_onboarding_tour/tour_step.tsx | 42 +++++- .../use_add_to_case_actions.tsx | 16 ++- .../timeline/body/actions/index.test.tsx | 129 +++++++++++++++++- .../timeline/body/actions/index.tsx | 7 +- 8 files changed, 276 insertions(+), 40 deletions(-) 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 index eb30e20f1318e..483d9c30cb82c 100644 --- 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 @@ -105,7 +105,7 @@ It was important that the `EuiTourStep` **anchor** is in the DOM when the tour s ``` createCaseFlyout.open({ attachments: caseAttachments, - ...(isTourShown(SecurityStepId.alertsCases) && activeStep === 4 + ...(isTourShown(SecurityStepId.alertsCases) && activeStep === AlertsCasesTourSteps.addAlertToCase ? { headerContent: ( // isTourAnchor=true no matter what in order to @@ -132,7 +132,7 @@ It was important that the `EuiTourStep` **anchor** is in the DOM when the tour s So we utilize the `useTourContext` to do the following check and increment the step in `handleAddToNewCaseClick`: ``` - if (isTourShown(SecurityStepId.alertsCases) && activeStep === 4) { + if (isTourShown(SecurityStepId.alertsCases) && activeStep === AlertsCasesTourSteps.addAlertToCase) { incrementStep(SecurityStepId.alertsCases); } ``` 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 index 43f6ca15b33cb..80cadec5d04be 100644 --- 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 @@ -19,7 +19,7 @@ import { securityTourConfig, SecurityStepId } from './tour_config'; export interface TourContextValue { activeStep: number; endTourStep: (stepId: SecurityStepId) => void; - incrementStep: (stepId: SecurityStepId, step?: number) => void; + incrementStep: (stepId: SecurityStepId) => void; isTourShown: (stepId: SecurityStepId) => boolean; } 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 index f7ed05be4c418..0a00c25417f83 100644 --- 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 @@ -14,6 +14,15 @@ export const enum SecurityStepId { alertsCases = 'alertsCases', } +export const enum AlertsCasesTourSteps { + none = 0, + pointToAlertName = 1, + expandEvent = 2, + reviewAlertDetailsFlyout = 3, + addAlertToCase = 4, + createCase = 5, +} + export type StepConfig = Pick< EuiTourStepProps, 'step' | 'content' | 'anchorPosition' | 'title' | 'initialFocus' | 'anchor' @@ -40,7 +49,7 @@ export const getTourAnchor = (step: number, stepId: SecurityStepId) => const alertsCasesConfig: StepConfig[] = [ { ...defaultConfig, - step: 1, + step: AlertsCasesTourSteps.pointToAlertName, title: i18n.translate('xpack.securitySolution.guided_onboarding.tour.ruleNameStep.tourTitle', { defaultMessage: 'Test alert for practice', }), @@ -52,12 +61,12 @@ const alertsCasesConfig: StepConfig[] = [ } ), anchorPosition: 'downCenter', - dataTestSubj: getTourAnchor(1, SecurityStepId.alertsCases), + dataTestSubj: getTourAnchor(AlertsCasesTourSteps.pointToAlertName, SecurityStepId.alertsCases), initialFocus: `button[tour-step="nextButton"]`, }, { ...defaultConfig, - step: 2, + step: AlertsCasesTourSteps.expandEvent, title: i18n.translate('xpack.securitySolution.guided_onboarding.tour.openFlyout.tourTitle', { defaultMessage: 'Review the alert details', }), @@ -69,12 +78,12 @@ const alertsCasesConfig: StepConfig[] = [ } ), anchorPosition: 'rightUp', - dataTestSubj: getTourAnchor(2, SecurityStepId.alertsCases), + dataTestSubj: getTourAnchor(AlertsCasesTourSteps.expandEvent, SecurityStepId.alertsCases), hideNextButton: true, }, { ...defaultConfig, - step: 3, + step: AlertsCasesTourSteps.reviewAlertDetailsFlyout, title: i18n.translate( 'xpack.securitySolution.guided_onboarding.tour.flyoutOverview.tourTitle', { @@ -89,13 +98,19 @@ const alertsCasesConfig: StepConfig[] = [ } ), // needs to use anchor to properly place tour step - anchor: `[tour-step="${getTourAnchor(3, SecurityStepId.alertsCases)}"] .euiTabs`, + anchor: `[tour-step="${getTourAnchor( + AlertsCasesTourSteps.reviewAlertDetailsFlyout, + SecurityStepId.alertsCases + )}"] .euiTabs`, anchorPosition: 'leftUp', - dataTestSubj: getTourAnchor(3, SecurityStepId.alertsCases), + dataTestSubj: getTourAnchor( + AlertsCasesTourSteps.reviewAlertDetailsFlyout, + SecurityStepId.alertsCases + ), }, { ...defaultConfig, - step: 4, + step: AlertsCasesTourSteps.addAlertToCase, title: i18n.translate('xpack.securitySolution.guided_onboarding.tour.addToCase.tourTitle', { defaultMessage: 'Create a case', }), @@ -103,12 +118,12 @@ const alertsCasesConfig: StepConfig[] = [ defaultMessage: 'From the Take action menu, add the alert to a new case.', }), anchorPosition: 'upRight', - dataTestSubj: getTourAnchor(4, SecurityStepId.alertsCases), + dataTestSubj: getTourAnchor(AlertsCasesTourSteps.addAlertToCase, SecurityStepId.alertsCases), hideNextButton: true, }, { ...defaultConfig, - step: 5, + step: AlertsCasesTourSteps.createCase, title: i18n.translate('xpack.securitySolution.guided_onboarding.tour.createCase.tourTitle', { defaultMessage: `Add details`, }), @@ -120,7 +135,7 @@ const alertsCasesConfig: StepConfig[] = [ ), anchor: `[data-test-subj="create-case-flyout"]`, anchorPosition: 'leftUp', - dataTestSubj: getTourAnchor(5, SecurityStepId.alertsCases), + dataTestSubj: getTourAnchor(AlertsCasesTourSteps.createCase, SecurityStepId.alertsCases), hideNextButton: true, }, ]; 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 index 04f2cfd6a4311..90f8b6de7c2f8 100644 --- 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 @@ -9,6 +9,12 @@ import { render } from '@testing-library/react'; import { GuidedOnboardingTourStep, SecurityTourStep } from './tour_step'; import { SecurityStepId } from './tour_config'; import { useTourContext } from './tour'; +import { mockGlobalState, SUB_PLUGINS_REDUCER, TestProviders } from '../../mock'; +import { TimelineId } from '../../../../common/types'; +import { createStore } from '../../store'; +import { tGridReducer } from '@kbn/timelines-plugin/public'; +import { kibanaObservable } from '@kbn/timelines-plugin/public/mock'; +import { createSecuritySolutionStorageMock } from '@kbn/timelines-plugin/public/mock/mock_local_storage'; jest.mock('./tour'); const mockTourStep = jest @@ -43,7 +49,8 @@ describe('GuidedOnboardingTourStep', () => { }); it('renders as a tour step', () => { const { getByTestId } = render( - {mockChildren} + {mockChildren}, + { wrapper: TestProviders } ); const tourStep = getByTestId('tourStepMock'); const header = getByTestId('h1'); @@ -54,7 +61,8 @@ describe('GuidedOnboardingTourStep', () => { const { getByTestId, queryByTestId } = render( {mockChildren} - + , + { wrapper: TestProviders } ); const tourStep = queryByTestId('tourStepMock'); const header = getByTestId('h1'); @@ -83,7 +91,8 @@ describe('SecurityTourStep', () => { render( {mockChildren} - + , + { wrapper: TestProviders } ); expect(mockTourStep).not.toHaveBeenCalled(); }); @@ -92,7 +101,8 @@ describe('SecurityTourStep', () => { render( {mockChildren} - + , + { wrapper: TestProviders } ); expect(mockTourStep).not.toHaveBeenCalled(); }); @@ -103,12 +113,16 @@ describe('SecurityTourStep', () => { incrementStep: jest.fn(), isTourShown: () => false, }); - render({mockChildren}); + render({mockChildren}, { + wrapper: TestProviders, + }); expect(mockTourStep).not.toHaveBeenCalled(); }); it('renders tour step with correct number of steppers', () => { - render({mockChildren}); + render({mockChildren}, { + wrapper: TestProviders, + }); const mockCall = { ...mockTourStep.mock.calls[0][0] }; expect(mockCall.step).toEqual(1); expect(mockCall.stepsTotal).toEqual(5); @@ -118,7 +132,8 @@ describe('SecurityTourStep', () => { render( {mockChildren} - + , + { wrapper: TestProviders } ); const mockCall = { ...mockTourStep.mock.calls[0][0] }; expect(mockCall.step).toEqual(5); @@ -134,7 +149,8 @@ describe('SecurityTourStep', () => { render( {mockChildren} - + , + { wrapper: TestProviders } ); const mockCall = { ...mockTourStep.mock.calls[0][0] }; expect(mockCall.footerAction).toMatchInlineSnapshot(` @@ -163,7 +179,8 @@ describe('SecurityTourStep', () => { const { container } = render( {mockChildren} - + , + { wrapper: TestProviders } ); const selectParent = container.querySelector( `[data-test-subj="tourStepMock"] [data-test-subj="h1"]` @@ -184,7 +201,8 @@ describe('SecurityTourStep', () => { const { container } = render( {mockChildren} - + , + { wrapper: TestProviders } ); const selectParent = container.querySelector( `[data-test-subj="tourStepMock"] [data-test-subj="h1"]` @@ -197,13 +215,17 @@ describe('SecurityTourStep', () => { }); it('if a tour step does not have children and has anchor, only render tour step', () => { - const { getByTestId } = render(); + const { getByTestId } = render( + , + { wrapper: TestProviders } + ); expect(getByTestId('tourStepMock')).toBeInTheDocument(); }); it('if a tour step does not have children and does not have anchor, render nothing', () => { const { queryByTestId } = render( - + , + { wrapper: TestProviders } ); expect(queryByTestId('tourStepMock')).not.toBeInTheDocument(); }); @@ -217,9 +239,40 @@ describe('SecurityTourStep', () => { render( {mockChildren} - + , + { wrapper: TestProviders } ); const mockCall = { ...mockTourStep.mock.calls[0][0] }; expect(mockCall.footerAction).toMatchInlineSnapshot(``); }); + + it('does not render step if timeline is open', () => { + const mockstate = { + ...mockGlobalState, + timeline: { + ...mockGlobalState.timeline, + timelineById: { + [TimelineId.active]: { + ...mockGlobalState.timeline.timelineById.test, + show: true, + }, + }, + }, + }; + const { storage } = createSecuritySolutionStorageMock(); + const mockStore = createStore( + mockstate, + SUB_PLUGINS_REDUCER, + { dataTable: tGridReducer }, + kibanaObservable, + storage + ); + + render( + + {mockChildren} + + ); + expect(mockTourStep).not.toHaveBeenCalled(); + }); }); 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 index ef07c5ce44a42..b7ade00021bad 100644 --- 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 @@ -11,26 +11,54 @@ import type { EuiTourStepProps } from '@elastic/eui'; import { EuiButton, EuiImage, EuiSpacer, EuiText, EuiTourStep } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import styled from 'styled-components'; +import { useShallowEqualSelector } from '../../hooks/use_selector'; +import { TimelineId } from '../../../../common/types'; +import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; +import { timelineSelectors } from '../../../timelines/store/timeline'; import { useTourContext } from './tour'; -import { securityTourConfig, SecurityStepId } from './tour_config'; +import { AlertsCasesTourSteps, SecurityStepId, securityTourConfig } from './tour_config'; + interface SecurityTourStep { children?: React.ReactElement; step: number; stepId: SecurityStepId; } +const isStepExternallyMounted = (stepId: SecurityStepId, step: number) => + step === AlertsCasesTourSteps.createCase && stepId === SecurityStepId.alertsCases; + +const StyledTourStep = styled(EuiTourStep)` + &.euiPopover__panel[data-popover-open] { + z-index: ${({ step, stepId }) => + isStepExternallyMounted(stepId, step) ? '9000 !important' : '1000 !important'}; + } +`; + 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 getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const showTimeline = useShallowEqualSelector( + (state) => (getTimeline(state, TimelineId.active) ?? timelineDefaults).show + ); + const onClick = useCallback(() => incrementStep(stepId), [incrementStep, stepId]); - // step === 5 && stepId === SecurityStepId.alertsCases is in Cases app and out of context. + + // step === AlertsCasesTourSteps.createCase && 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)) { + const overrideContext = isStepExternallyMounted(stepId, step); + + if ( + tourStep == null || + ((step !== activeStep || !isTourShown(stepId)) && !overrideContext) || + showTimeline + ) { return children ? children : null; } @@ -89,11 +117,13 @@ export const SecurityTourStep = ({ children, step, stepId }: SecurityTourStep) = // see type EuiTourStepAnchorProps return anchor != null ? ( <> - + <>{children} ) : children != null ? ( - {children} + + {children} + ) : null; }; 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 70455fa342ab5..9f6fd3dd56104 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 @@ -10,7 +10,10 @@ 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 { + AlertsCasesTourSteps, + 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'; @@ -80,7 +83,11 @@ export const useAddToCaseActions = ({ onMenuItemClick(); createCaseFlyout.open({ attachments: caseAttachments, - ...(isTourShown(SecurityStepId.alertsCases) && activeStep === 4 + // activeStep will be 4 on first render because not yet incremented + // if the user closes the flyout without completing the form and comes back, we will be at step 5 + ...(isTourShown(SecurityStepId.alertsCases) && + (activeStep === AlertsCasesTourSteps.addAlertToCase || + activeStep === AlertsCasesTourSteps.createCase) ? { headerContent: ( // isTourAnchor=true no matter what in order to @@ -90,7 +97,10 @@ export const useAddToCaseActions = ({ } : {}), }); - if (isTourShown(SecurityStepId.alertsCases) && activeStep === 4) { + if ( + isTourShown(SecurityStepId.alertsCases) && + activeStep === AlertsCasesTourSteps.addAlertToCase + ) { incrementStep(SecurityStepId.alertsCases); } }, [onMenuItemClick, createCaseFlyout, caseAttachments, isTourShown, activeStep, incrementStep]); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx index 9e8ea89d9175b..e34227f0bfe8f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx @@ -14,7 +14,14 @@ import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use import { mockCasesContract } from '@kbn/cases-plugin/public/mocks'; import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector'; import { licenseService } from '../../../../../common/hooks/use_license'; - +import { useTourContext } from '../../../../../common/components/guided_onboarding_tour'; +import { + GuidedOnboardingTourStep, + SecurityTourStep, +} from '../../../../../common/components/guided_onboarding_tour/tour_step'; +import { SecurityStepId } from '../../../../../common/components/guided_onboarding_tour/tour_config'; + +jest.mock('../../../../../common/components/guided_onboarding_tour'); jest.mock('../../../../../detections/components/user_info', () => ({ useUserData: jest.fn().mockReturnValue([{ canUserCRUD: true, hasIndexWrite: true }]), })); @@ -106,6 +113,11 @@ const defaultProps = { describe('Actions', () => { beforeAll(() => { + (useTourContext as jest.Mock).mockReturnValue({ + activeStep: 1, + incrementStep: () => null, + isTourShown: () => false, + }); (useShallowEqualSelector as jest.Mock).mockReturnValue(mockTimelineModel); }); @@ -140,6 +152,121 @@ describe('Actions', () => { expect(wrapper.find('[data-test-subj="select-event"]').exists()).toBe(false); }); + describe('Guided Onboarding Step', () => { + const incrementStepMock = jest.fn(); + beforeEach(() => { + (useTourContext as jest.Mock).mockReturnValue({ + activeStep: 2, + incrementStep: incrementStepMock, + isTourShown: () => true, + }); + jest.clearAllMocks(); + }); + + const ecsData = { + ...mockTimelineData[0].ecs, + kibana: { alert: { rule: { uuid: ['123'], parameters: {} } } }, + }; + const isTourAnchorConditions: { [key: string]: unknown } = { + ecsData, + timelineId: TableId.alertsOnAlertsPage, + ariaRowindex: 1, + }; + + test('if isTourShown is false [isTourAnchor=false], SecurityTourStep is not active', () => { + (useTourContext as jest.Mock).mockReturnValue({ + activeStep: 2, + incrementStep: jest.fn(), + isTourShown: () => false, + }); + + const wrapper = mount( + + + + ); + + expect(wrapper.find(GuidedOnboardingTourStep).exists()).toEqual(true); + expect(wrapper.find(SecurityTourStep).exists()).toEqual(false); + }); + + test('if all conditions make isTourAnchor=true, SecurityTourStep is active', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(GuidedOnboardingTourStep).exists()).toEqual(true); + expect(wrapper.find(SecurityTourStep).exists()).toEqual(true); + }); + + test('on expand event click and SecurityTourStep is active, incrementStep', () => { + const wrapper = mount( + + + + ); + + wrapper.find('[data-test-subj="expand-event"]').first().simulate('click'); + + expect(incrementStepMock).toHaveBeenCalledWith(SecurityStepId.alertsCases); + }); + + test('on expand event click and SecurityTourStep is active, but step is not 2, do not incrementStep', () => { + (useTourContext as jest.Mock).mockReturnValue({ + activeStep: 1, + incrementStep: incrementStepMock, + isTourShown: () => true, + }); + + const wrapper = mount( + + + + ); + + wrapper.find('[data-test-subj="expand-event"]').first().simulate('click'); + + expect(incrementStepMock).not.toHaveBeenCalled(); + }); + + test('on expand event click and SecurityTourStep is not active, do not incrementStep', () => { + const wrapper = mount( + + + + ); + + wrapper.find('[data-test-subj="expand-event"]').first().simulate('click'); + + expect(incrementStepMock).not.toHaveBeenCalled(); + }); + + test('if isTourAnchor=false, SecurityTourStep is not active', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(GuidedOnboardingTourStep).exists()).toEqual(true); + expect(wrapper.find(SecurityTourStep).exists()).toEqual(false); + }); + describe.each(Object.keys(isTourAnchorConditions))('tour condition true: %s', (key: string) => { + it('Single condition does not make tour step exist', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(GuidedOnboardingTourStep).exists()).toEqual(true); + expect(wrapper.find(SecurityTourStep).exists()).toEqual(false); + }); + }); + }); + describe('Alert context menu enabled?', () => { test('it disables for eventType=raw', () => { const wrapper = mount( 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 5454642ea5892..26ffd4ea8e28e 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 @@ -203,7 +203,7 @@ const ActionsComponent: React.FC = ({ scopedActions, ]); - const { isTourShown, incrementStep } = useTourContext(); + const { activeStep, isTourShown, incrementStep } = useTourContext(); const isTourAnchor = useMemo( () => @@ -215,11 +215,12 @@ const ActionsComponent: React.FC = ({ ); const onExpandEvent = useCallback(() => { - if (isTourAnchor) { + const isStep2Active = activeStep === 2 && isTourShown(SecurityStepId.alertsCases); + if (isTourAnchor && isStep2Active) { incrementStep(SecurityStepId.alertsCases); } onEventDetailsPanelOpened(); - }, [incrementStep, isTourAnchor, onEventDetailsPanelOpened]); + }, [activeStep, incrementStep, isTourAnchor, isTourShown, onEventDetailsPanelOpened]); return (