[Security Solution][Alert Details] - remove flyout tour introduced in 8.14 (#198708)

## Summary

We [added](https://github.com/elastic/kibana/pull/180318) a guided tour
to the expandable flyout back in `8.14`. It is time to remove it as
enough users have seen it.

No UI changes other than the tour not showing up.

### How to test

- clear local storage (more specifically remove the
`securitySolution.documentDetails.newFeaturesTour.v8.14` key
- open a alert or event details flyout and verify that the tour does not
show up

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

Should help solve https://github.com/elastic/kibana/issues/197492
This commit is contained in:
Philippe Oberti 2024-11-04 15:02:07 -06:00 committed by GitHub
parent 1a100a4f52
commit 0aec5a82db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 14 additions and 946 deletions

View file

@ -67,7 +67,6 @@ export const internalRequest = <T = unknown>({
const NEW_FEATURES_TOUR_STORAGE_KEYS = {
RULE_MANAGEMENT_PAGE: 'securitySolution.rulesManagementPage.newFeaturesTour.v8.9',
TIMELINES: 'securitySolution.security.timelineFlyoutHeader.saveTimelineTour',
FLYOUT: 'securitySolution.documentDetails.newFeaturesTour.v8.14',
};
const disableNewFeaturesTours = (window: Window) => {

View file

@ -45,8 +45,6 @@ export enum NAV_SEARCH_INPUT_OSQUERY_RESULTS {
export const NEW_FEATURES_TOUR_STORAGE_KEYS = {
RULE_MANAGEMENT_PAGE: 'securitySolution.rulesManagementPage.newFeaturesTour.v8.13',
TIMELINES: 'securitySolution.security.timelineFlyoutHeader.saveTimelineTour',
TIMELINE: 'securitySolution.timeline.newFeaturesTour.v8.12',
FLYOUT: 'securitySolution.documentDetails.newFeaturesTour.v8.14',
KNOWLEDGE_BASE: 'elasticAssistant.knowledgeBase.newFeaturesTour.v8.16',
};

View file

@ -424,7 +424,6 @@ export const RULES_TABLE_MAX_PAGE_SIZE = 100;
export const NEW_FEATURES_TOUR_STORAGE_KEYS = {
RULE_MANAGEMENT_PAGE: 'securitySolution.rulesManagementPage.newFeaturesTour.v8.13',
TIMELINES: 'securitySolution.security.timelineFlyoutHeader.saveTimelineTour',
FLYOUT: 'securitySolution.documentDetails.newFeaturesTour.v8.14',
};
export const RULE_DETAILS_EXECUTION_LOG_TABLE_SHOW_METRIC_COLUMNS_STORAGE_KEY =

View file

@ -1,122 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { render, waitFor, fireEvent } from '@testing-library/react';
import { LeftPanelTour } from './tour';
import { DocumentDetailsContext } from '../../shared/context';
import { mockContextValue } from '../../shared/mocks/mock_context';
import {
createMockStore,
createSecuritySolutionStorageMock,
TestProviders,
} from '../../../../common/mock';
import { useKibana as mockUseKibana } from '../../../../common/lib/kibana/__mocks__';
import { useKibana } from '../../../../common/lib/kibana';
import { FLYOUT_TOUR_CONFIG_ANCHORS } from '../../shared/utils/tour_step_config';
import { FLYOUT_TOUR_TEST_ID } from '../../shared/components/test_ids';
import { useWhichFlyout } from '../../shared/hooks/use_which_flyout';
import { Flyouts } from '../../shared/constants/flyouts';
jest.mock('../../../../common/lib/kibana');
jest.mock('../../shared/hooks/use_which_flyout');
const mockedUseKibana = mockUseKibana();
const { storage: storageMock } = createSecuritySolutionStorageMock();
const mockStore = createMockStore(undefined, undefined, undefined, {
...storageMock,
});
const renderLeftPanelTour = (context: DocumentDetailsContext = mockContextValue) =>
render(
<TestProviders store={mockStore}>
<DocumentDetailsContext.Provider value={context}>
<LeftPanelTour />
{Object.values(FLYOUT_TOUR_CONFIG_ANCHORS).map((i, idx) => (
<div key={idx} data-test-subj={i} />
))}
</DocumentDetailsContext.Provider>
</TestProviders>
);
describe('<LeftPanelTour />', () => {
beforeEach(() => {
(useKibana as jest.Mock).mockReturnValue({
...mockedUseKibana,
services: {
...mockedUseKibana.services,
storage: storageMock,
},
});
(useWhichFlyout as jest.Mock).mockReturnValue(Flyouts.securitySolution);
storageMock.clear();
});
it('should render left panel tour for alerts starting as step 4', async () => {
storageMock.set('securitySolution.documentDetails.newFeaturesTour.v8.14', {
currentTourStep: 4,
isTourActive: true,
});
const { getByText, getByTestId } = renderLeftPanelTour();
await waitFor(() => {
expect(getByTestId(`${FLYOUT_TOUR_TEST_ID}-4`)).toBeVisible();
});
fireEvent.click(getByText('Next'));
await waitFor(() => {
expect(getByTestId(`${FLYOUT_TOUR_TEST_ID}-5`)).toBeVisible();
});
await waitFor(() => {
expect(getByText('Finish')).toBeVisible();
});
});
it('should not render left panel tour for preview', () => {
storageMock.set('securitySolution.documentDetails.newFeaturesTour.v8.14', {
currentTourStep: 3,
isTourActive: true,
});
const { queryByTestId, queryByText } = renderLeftPanelTour({
...mockContextValue,
isPreview: true,
});
expect(queryByTestId(`${FLYOUT_TOUR_TEST_ID}-4`)).not.toBeInTheDocument();
expect(queryByText('Next')).not.toBeInTheDocument();
});
it('should not render left panel tour for non-alerts', async () => {
storageMock.set('securitySolution.documentDetails.newFeaturesTour.v8.14', {
currentTourStep: 3,
isTourActive: true,
});
const { queryByTestId, queryByText } = renderLeftPanelTour({
...mockContextValue,
getFieldsData: () => '',
});
expect(queryByTestId(`${FLYOUT_TOUR_TEST_ID}-4`)).not.toBeInTheDocument();
expect(queryByText('Next')).not.toBeInTheDocument();
});
it('should not render left panel tour for flyout in timeline', () => {
(useWhichFlyout as jest.Mock).mockReturnValue(Flyouts.timeline);
storageMock.set('securitySolution.documentDetails.newFeaturesTour.v8.14', {
currentTourStep: 3,
isTourActive: true,
});
const { queryByTestId, queryByText } = renderLeftPanelTour({
...mockContextValue,
isPreview: true,
});
expect(queryByTestId(`${FLYOUT_TOUR_TEST_ID}-4`)).not.toBeInTheDocument();
expect(queryByText('Next')).not.toBeInTheDocument();
});
});

View file

@ -1,32 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { memo, useMemo } from 'react';
import { useWhichFlyout } from '../../shared/hooks/use_which_flyout';
import { getField } from '../../shared/utils';
import { EventKind } from '../../shared/constants/event_kinds';
import { useDocumentDetailsContext } from '../../shared/context';
import { getLeftSectionTourSteps } from '../../shared/utils/tour_step_config';
import { Flyouts } from '../../shared/constants/flyouts';
import { FlyoutTour } from '../../shared/components/flyout_tour';
/**
* Guided tour for the left panel in details flyout
*/
export const LeftPanelTour = memo(() => {
const { getFieldsData, isPreview } = useDocumentDetailsContext();
const eventKind = getField(getFieldsData('event.kind'));
const isAlert = eventKind === EventKind.signal;
const isTimelineFlyoutOpen = useWhichFlyout() === Flyouts.timeline;
const showTour = isAlert && !isPreview && !isTimelineFlyoutOpen;
const tourStepContent = useMemo(() => getLeftSectionTourSteps(), []);
return showTour ? <FlyoutTour tourStepContent={tourStepContent} totalSteps={5} /> : null;
});
LeftPanelTour.displayName = 'LeftPanelTour';

View file

@ -22,7 +22,6 @@ import { getField } from '../shared/utils';
import { EventKind } from '../shared/constants/event_kinds';
import { useDocumentDetailsContext } from '../shared/context';
import type { DocumentDetailsProps } from '../shared/types';
import { LeftPanelTour } from './components/tour';
export type LeftPanelPaths = 'visualize' | 'insights' | 'investigation' | 'response' | 'notes';
export const LeftPanelVisualizeTab: LeftPanelPaths = 'visualize';
@ -85,7 +84,6 @@ export const LeftPanel: FC<Partial<DocumentDetailsProps>> = memo(({ path }) => {
return (
<>
<LeftPanelTour />
<PanelHeader
selectedTabId={selectedTabId}
setSelectedTabId={setSelectedTabId}

View file

@ -14,10 +14,8 @@ import { useExpandableFlyoutApi, useExpandableFlyoutState } from '@kbn/expandabl
import { useKibana } from '../../../../common/lib/kibana';
import {
INSIGHTS_TAB_BUTTON_GROUP_TEST_ID,
INSIGHTS_TAB_ENTITIES_BUTTON_LABEL_TEST_ID,
INSIGHTS_TAB_ENTITIES_BUTTON_TEST_ID,
INSIGHTS_TAB_THREAT_INTELLIGENCE_BUTTON_TEST_ID,
INSIGHTS_TAB_PREVALENCE_BUTTON_LABEL_TEST_ID,
INSIGHTS_TAB_PREVALENCE_BUTTON_TEST_ID,
INSIGHTS_TAB_CORRELATIONS_BUTTON_TEST_ID,
} from './test_ids';
@ -40,12 +38,10 @@ const insightsButtons: EuiButtonGroupOptionProps[] = [
{
id: ENTITIES_TAB_ID,
label: (
<div data-test-subj={INSIGHTS_TAB_ENTITIES_BUTTON_LABEL_TEST_ID}>
<FormattedMessage
id="xpack.securitySolution.flyout.left.insights.entitiesButtonLabel"
defaultMessage="Entities"
/>
</div>
<FormattedMessage
id="xpack.securitySolution.flyout.left.insights.entitiesButtonLabel"
defaultMessage="Entities"
/>
),
'data-test-subj': INSIGHTS_TAB_ENTITIES_BUTTON_TEST_ID,
},
@ -62,12 +58,10 @@ const insightsButtons: EuiButtonGroupOptionProps[] = [
{
id: PREVALENCE_TAB_ID,
label: (
<div data-test-subj={INSIGHTS_TAB_PREVALENCE_BUTTON_LABEL_TEST_ID}>
<FormattedMessage
id="xpack.securitySolution.flyout.left.insights.prevalenceButtonLabel"
defaultMessage="Prevalence"
/>
</div>
<FormattedMessage
id="xpack.securitySolution.flyout.left.insights.prevalenceButtonLabel"
defaultMessage="Prevalence"
/>
),
'data-test-subj': INSIGHTS_TAB_PREVALENCE_BUTTON_TEST_ID,
},

View file

@ -17,12 +17,8 @@ const INSIGHTS_TAB_TEST_ID = `${PREFIX}InsightsTab` as const;
export const INSIGHTS_TAB_BUTTON_GROUP_TEST_ID = `${INSIGHTS_TAB_TEST_ID}ButtonGroup` as const;
export const INSIGHTS_TAB_ENTITIES_BUTTON_TEST_ID =
`${INSIGHTS_TAB_TEST_ID}EntitiesButton` as const;
export const INSIGHTS_TAB_ENTITIES_BUTTON_LABEL_TEST_ID =
`${INSIGHTS_TAB_TEST_ID}Entities` as const;
export const INSIGHTS_TAB_THREAT_INTELLIGENCE_BUTTON_TEST_ID =
`${INSIGHTS_TAB_TEST_ID}ThreatIntelligenceButton` as const;
export const INSIGHTS_TAB_PREVALENCE_BUTTON_LABEL_TEST_ID =
`${INSIGHTS_TAB_TEST_ID}Prevalence` as const;
export const INSIGHTS_TAB_PREVALENCE_BUTTON_TEST_ID =
`${INSIGHTS_TAB_TEST_ID}PrevalenceButton` as const;
export const INSIGHTS_TAB_CORRELATIONS_BUTTON_TEST_ID =

View file

@ -1,125 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { render, waitFor, fireEvent } from '@testing-library/react';
import { RightPanelTour } from './tour';
import { DocumentDetailsContext } from '../../shared/context';
import { mockContextValue } from '../../shared/mocks/mock_context';
import {
createMockStore,
createSecuritySolutionStorageMock,
TestProviders,
} from '../../../../common/mock';
import { useKibana as mockUseKibana } from '../../../../common/lib/kibana/__mocks__';
import { useKibana } from '../../../../common/lib/kibana';
import { FLYOUT_TOUR_CONFIG_ANCHORS } from '../../shared/utils/tour_step_config';
import { FLYOUT_TOUR_TEST_ID } from '../../shared/components/test_ids';
import { useTourContext } from '../../../../common/components/guided_onboarding_tour/tour';
import { casesPluginMock } from '@kbn/cases-plugin/public/mocks';
import { useWhichFlyout } from '../../shared/hooks/use_which_flyout';
import { Flyouts } from '../../shared/constants/flyouts';
jest.mock('../../../../common/lib/kibana');
jest.mock('../../shared/hooks/use_which_flyout');
jest.mock('../../../../common/components/guided_onboarding_tour/tour');
const mockedUseKibana = mockUseKibana();
const { storage: storageMock } = createSecuritySolutionStorageMock();
const mockStore = createMockStore(undefined, undefined, undefined, {
...storageMock,
});
const mockCasesContract = casesPluginMock.createStartContract();
const mockUseIsAddToCaseOpen = mockCasesContract.hooks.useIsAddToCaseOpen as jest.Mock;
mockUseIsAddToCaseOpen.mockReturnValue(false);
const renderRightPanelTour = (context: DocumentDetailsContext = mockContextValue) =>
render(
<TestProviders store={mockStore}>
<DocumentDetailsContext.Provider value={context}>
<RightPanelTour />
{Object.values(FLYOUT_TOUR_CONFIG_ANCHORS).map((i, idx) => (
<div key={idx} data-test-subj={i} />
))}
</DocumentDetailsContext.Provider>
</TestProviders>
);
describe('<RightPanelTour />', () => {
beforeEach(() => {
(useKibana as jest.Mock).mockReturnValue({
...mockedUseKibana,
services: {
...mockedUseKibana.services,
storage: storageMock,
cases: mockCasesContract,
},
});
(useWhichFlyout as jest.Mock).mockReturnValue(Flyouts.securitySolution);
(useTourContext as jest.Mock).mockReturnValue({ isTourShown: jest.fn(() => false) });
storageMock.clear();
});
it('should render tour for alerts', async () => {
const { getByText, getByTestId } = renderRightPanelTour();
await waitFor(() => {
expect(getByTestId(`${FLYOUT_TOUR_TEST_ID}-1`)).toBeVisible();
});
fireEvent.click(getByText('Next'));
await waitFor(() => {
expect(getByTestId(`${FLYOUT_TOUR_TEST_ID}-2`)).toBeVisible();
});
fireEvent.click(getByText('Next'));
await waitFor(() => {
expect(getByTestId(`${FLYOUT_TOUR_TEST_ID}-3`)).toBeVisible();
});
fireEvent.click(getByText('Next'));
});
it('should not render tour for preview', () => {
const { queryByTestId, queryByText } = renderRightPanelTour({
...mockContextValue,
isPreview: true,
});
expect(queryByTestId(`${FLYOUT_TOUR_TEST_ID}-1`)).not.toBeInTheDocument();
expect(queryByText('Next')).not.toBeInTheDocument();
});
it('should not render tour when guided onboarding tour is active', () => {
(useTourContext as jest.Mock).mockReturnValue({ isTourShown: jest.fn(() => true) });
const { queryByText, queryByTestId } = renderRightPanelTour({
...mockContextValue,
getFieldsData: () => '',
});
expect(queryByTestId(`${FLYOUT_TOUR_TEST_ID}-1`)).not.toBeInTheDocument();
expect(queryByText('Next')).not.toBeInTheDocument();
});
it('should not render tour when case modal is open', () => {
mockUseIsAddToCaseOpen.mockReturnValue(true);
const { queryByText, queryByTestId } = renderRightPanelTour({
...mockContextValue,
getFieldsData: () => '',
});
expect(queryByTestId(`${FLYOUT_TOUR_TEST_ID}-1`)).not.toBeInTheDocument();
expect(queryByText('Next')).not.toBeInTheDocument();
});
it('should not render tour for flyout in timeline', () => {
(useWhichFlyout as jest.Mock).mockReturnValue(Flyouts.timeline);
const { queryByText, queryByTestId } = renderRightPanelTour({
...mockContextValue,
getFieldsData: () => '',
});
expect(queryByTestId(`${FLYOUT_TOUR_TEST_ID}-1`)).not.toBeInTheDocument();
expect(queryByText('Next')).not.toBeInTheDocument();
});
});

View file

@ -1,90 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { memo, useMemo, useCallback } from 'react';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { useWhichFlyout } from '../../shared/hooks/use_which_flyout';
import { Flyouts } from '../../shared/constants/flyouts';
import { useDocumentDetailsContext } from '../../shared/context';
import {
getRightSectionTourSteps,
getLeftSectionTourSteps,
} from '../../shared/utils/tour_step_config';
import { getField } from '../../shared/utils';
import {
DocumentDetailsLeftPanelKey,
DocumentDetailsRightPanelKey,
} from '../../shared/constants/panel_keys';
import { EventKind } from '../../shared/constants/event_kinds';
import { useTourContext } from '../../../../common/components/guided_onboarding_tour/tour';
import { SecurityStepId } from '../../../../common/components/guided_onboarding_tour/tour_config';
import { useKibana } from '../../../../common/lib/kibana';
import { FlyoutTour } from '../../shared/components/flyout_tour';
/**
* Guided tour for the right panel in details flyout
*/
export const RightPanelTour = memo(() => {
const { useIsAddToCaseOpen } = useKibana().services.cases.hooks;
const casesFlyoutExpanded = useIsAddToCaseOpen();
const { isTourShown: isGuidedOnboardingTourShown } = useTourContext();
const { openLeftPanel, openRightPanel } = useExpandableFlyoutApi();
const { eventId, indexName, scopeId, isPreview, getFieldsData } = useDocumentDetailsContext();
const eventKind = getField(getFieldsData('event.kind'));
const isAlert = eventKind === EventKind.signal;
const isTimelineFlyoutOpen = useWhichFlyout() === Flyouts.timeline;
const showTour =
isAlert &&
!isPreview &&
!isTimelineFlyoutOpen &&
!isGuidedOnboardingTourShown(SecurityStepId.alertsCases) &&
!casesFlyoutExpanded;
const goToLeftPanel = useCallback(() => {
openLeftPanel({
id: DocumentDetailsLeftPanelKey,
params: {
id: eventId,
indexName,
scopeId,
},
});
}, [eventId, indexName, scopeId, openLeftPanel]);
const goToOverviewTab = useCallback(() => {
openRightPanel({
id: DocumentDetailsRightPanelKey,
path: { tab: 'overview' },
params: {
id: eventId,
indexName,
scopeId,
},
});
}, [eventId, indexName, scopeId, openRightPanel]);
const tourStepContent = useMemo(
// we append the left tour steps here to support the scenarios where the flyout left section is already expanded when starting the tour
() => [...getRightSectionTourSteps(), ...getLeftSectionTourSteps()],
[]
);
return showTour ? (
<FlyoutTour
tourStepContent={tourStepContent}
totalSteps={5}
goToOverviewTab={goToOverviewTab}
goToLeftPanel={goToLeftPanel}
/>
) : null;
});
RightPanelTour.displayName = 'RightPanelTour';

View file

@ -17,7 +17,6 @@ import type { DocumentDetailsProps } from '../shared/types';
import { PanelNavigation } from './navigation';
import { PanelHeader } from './header';
import { PanelContent } from './content';
import { RightPanelTour } from './components/tour';
import type { RightPanelTabType } from './tabs';
import { PanelFooter } from './footer';
import { useFlyoutIsExpandable } from './hooks/use_flyout_is_expandable';
@ -76,7 +75,6 @@ export const RightPanel: FC<Partial<DocumentDetailsProps>> = memo(({ path }) =>
return (
<>
{flyoutIsExpandable && <RightPanelTour />}
<PanelNavigation flyoutIsExpandable={flyoutIsExpandable} />
<PanelHeader
tabs={tabsDisplayed}

View file

@ -8,12 +8,7 @@
import type { ReactElement } from 'react';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import {
JSON_TAB_TEST_ID,
OVERVIEW_TAB_LABEL_TEST_ID,
OVERVIEW_TAB_TEST_ID,
TABLE_TAB_TEST_ID,
} from './test_ids';
import { JSON_TAB_TEST_ID, OVERVIEW_TAB_TEST_ID, TABLE_TAB_TEST_ID } from './test_ids';
import type { RightPanelPaths } from '.';
import { JsonTab } from './tabs/json_tab';
import { OverviewTab } from './tabs/overview_tab';
@ -30,12 +25,10 @@ export const overviewTab: RightPanelTabType = {
id: 'overview',
'data-test-subj': OVERVIEW_TAB_TEST_ID,
name: (
<div data-test-subj={OVERVIEW_TAB_LABEL_TEST_ID}>
<FormattedMessage
id="xpack.securitySolution.flyout.right.header.overviewTabLabel"
defaultMessage="Overview"
/>
</div>
<FormattedMessage
id="xpack.securitySolution.flyout.right.header.overviewTabLabel"
defaultMessage="Overview"
/>
),
content: <OverviewTab />,
};

View file

@ -12,6 +12,5 @@ export const FLYOUT_FOOTER_TEST_ID = `${PREFIX}Footer` as const;
export const FLYOUT_FOOTER_DEOPDOEN_BUTTON_TEST_ID =
`${FLYOUT_FOOTER_TEST_ID}DropdownButton` as const;
export const OVERVIEW_TAB_TEST_ID = `${PREFIX}OverviewTab` as const;
export const OVERVIEW_TAB_LABEL_TEST_ID = `${PREFIX}OverviewTabLabel` as const;
export const TABLE_TAB_TEST_ID = `${PREFIX}TableTab` as const;
export const JSON_TAB_TEST_ID = `${PREFIX}JsonTab` as const;

View file

@ -1,117 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { type FlyoutTourProps, FlyoutTour } from './flyout_tour';
import { render, waitFor, fireEvent } from '@testing-library/react';
import {
createMockStore,
createSecuritySolutionStorageMock,
TestProviders,
} from '../../../../common/mock';
import { useKibana as mockUseKibana } from '../../../../common/lib/kibana/__mocks__';
import { useKibana } from '../../../../common/lib/kibana';
import { FLYOUT_TOUR_TEST_ID } from './test_ids';
jest.mock('../../../../common/lib/kibana');
const mockedUseKibana = mockUseKibana();
const { storage: storageMock } = createSecuritySolutionStorageMock();
const mockStore = createMockStore(undefined, undefined, undefined, storageMock);
const content = [1, 2, 3, 4].map((i) => ({
title: `step${i}`,
content: <p>{`step${i}`}</p>,
stepNumber: i,
anchor: `step${i}`,
}));
const tourProps = {
tourStepContent: content,
totalSteps: 4,
};
const goToLeftPanel = jest.fn();
const goToOverviewTab = jest.fn();
const renderTour = (props: FlyoutTourProps) =>
render(
<TestProviders store={mockStore}>
<FlyoutTour {...props} />
<div data-test-subj="step1" />
<div data-test-subj="step2" />
<div data-test-subj="step3" />
<div data-test-subj="step4" />
</TestProviders>
);
describe('Flyout Tour', () => {
beforeEach(() => {
(useKibana as jest.Mock).mockReturnValue({
...mockedUseKibana,
services: {
...mockedUseKibana.services,
storage: storageMock,
},
});
storageMock.clear();
});
it('should render tour steps', async () => {
const wrapper = renderTour(tourProps);
await waitFor(() => {
expect(wrapper.getByTestId(`${FLYOUT_TOUR_TEST_ID}-1`)).toBeVisible();
});
fireEvent.click(wrapper.getByText('Next'));
await waitFor(() => {
expect(wrapper.getByTestId(`${FLYOUT_TOUR_TEST_ID}-2`)).toBeVisible();
});
fireEvent.click(wrapper.getByText('Next'));
await waitFor(() => {
expect(wrapper.getByTestId(`${FLYOUT_TOUR_TEST_ID}-3`)).toBeVisible();
});
fireEvent.click(wrapper.getByText('Next'));
await waitFor(() => {
expect(wrapper.getByTestId(`${FLYOUT_TOUR_TEST_ID}-4`)).toBeVisible();
});
await waitFor(() => {
expect(wrapper.getByText('Finish')).toBeVisible();
});
});
it('should call goToOverview at step 1', () => {
renderTour({
...tourProps,
goToOverviewTab,
});
expect(goToOverviewTab).toHaveBeenCalled();
});
it('should call goToLeftPanel when after step 3', async () => {
const wrapper = renderTour({
...tourProps,
goToLeftPanel,
});
await waitFor(() => {
expect(wrapper.getByTestId(`${FLYOUT_TOUR_TEST_ID}-1`)).toBeVisible();
});
fireEvent.click(wrapper.getByText('Next'));
await waitFor(() => {
expect(wrapper.getByTestId(`${FLYOUT_TOUR_TEST_ID}-2`)).toBeVisible();
});
fireEvent.click(wrapper.getByText('Next'));
await waitFor(() => {
expect(wrapper.getByTestId(`${FLYOUT_TOUR_TEST_ID}-3`)).toBeVisible();
});
fireEvent.click(wrapper.getByText('Next'));
expect(goToLeftPanel).toHaveBeenCalled();
});
});

View file

@ -1,160 +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.
*/
/*
* This timeline tour only valid for 8.12 release is not needed for 8.13
*
* */
import type { FC } from 'react';
import React, { useCallback, useState, useEffect } from 'react';
import { EuiButton, EuiButtonEmpty, EuiTourStep } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { tourConfig, type TourState } from '../utils/tour_step_config';
import type { FlyoutTourStepsProps } from '../utils/tour_step_config';
import { NEW_FEATURES_TOUR_STORAGE_KEYS } from '../../../../../common/constants';
import { useKibana } from '../../../../common/lib/kibana';
import { FLYOUT_TOUR_TEST_ID } from './test_ids';
export interface FlyoutTourProps {
/**
* Content to be displayed in each tour card
*/
tourStepContent: FlyoutTourStepsProps[];
/**
* Total number of tour steps
*/
totalSteps: number;
/**
* Callback to go to overview tab before tour
*/
goToOverviewTab?: () => void;
/**
* Callback to go to open left panel
*/
goToLeftPanel?: () => void;
}
const MAX_POPOVER_WIDTH = 500;
const TOUR_SUBTITLE = i18n.translate('xpack.securitySolution.flyout.tour.subtitle', {
defaultMessage: 'A redesigned alert experience',
});
/**
* Shared component that generates tour steps based on supplied tour step content.
* Supports tours being shown in different panels and manages state via local storage
*/
export const FlyoutTour: FC<FlyoutTourProps> = ({
tourStepContent,
totalSteps,
goToOverviewTab,
goToLeftPanel,
}) => {
const {
services: { storage },
} = useKibana();
const [tourState, setTourState] = useState<TourState>(() => {
const restoredTourState = storage.get(NEW_FEATURES_TOUR_STORAGE_KEYS.FLYOUT);
if (restoredTourState != null) {
return restoredTourState;
}
return tourConfig;
});
useEffect(() => {
storage.set(NEW_FEATURES_TOUR_STORAGE_KEYS.FLYOUT, tourState);
if (tourState.isTourActive && tourState.currentTourStep === 1 && goToOverviewTab) {
goToOverviewTab();
}
}, [storage, tourState, goToOverviewTab]);
const nextStep = useCallback(() => {
setTourState((prev) => {
if (prev.currentTourStep === 3 && goToLeftPanel) {
goToLeftPanel();
}
return {
...prev,
currentTourStep: prev.currentTourStep + 1,
};
});
}, [goToLeftPanel]);
const finishTour = useCallback(() => {
setTourState((prev) => {
return {
...prev,
isTourActive: false,
};
});
}, []);
const getFooterAction = useCallback(
(step: number) => {
// if it's the last step, we don't want to show the next button
return step === totalSteps ? (
<EuiButton color="success" size="s" onClick={finishTour}>
{i18n.translate('xpack.securitySolution.flyout.tour.finish.text', {
defaultMessage: 'Finish',
})}
</EuiButton>
) : (
[
<EuiButtonEmpty size="s" color="text" onClick={finishTour}>
{i18n.translate('xpack.securitySolution.flyout.tour.exit.text', {
defaultMessage: 'Exit',
})}
</EuiButtonEmpty>,
<EuiButton color="success" size="s" onClick={nextStep}>
{i18n.translate('xpack.securitySolution.flyout.tour.Next.text', {
defaultMessage: 'Next',
})}
</EuiButton>,
]
);
},
[finishTour, nextStep, totalSteps]
);
// Do not show tour if it is inactive
if (!tourState.isTourActive) {
return null;
}
return (
<>
{tourStepContent.map((steps) => {
const stepCount = steps.stepNumber;
if (tourState.currentTourStep !== stepCount) return null;
const panelProps = {
'data-test-subj': `${FLYOUT_TOUR_TEST_ID}-${stepCount}`,
};
return (
<EuiTourStep
panelProps={panelProps}
key={stepCount}
step={stepCount}
isStepOpen={tourState.isTourActive}
maxWidth={MAX_POPOVER_WIDTH}
stepsTotal={totalSteps}
onFinish={finishTour}
title={steps.title}
content={steps.content}
anchor={`[data-test-subj=${steps.anchor}]`}
anchorPosition={steps.anchorPosition}
footerAction={getFooterAction(stepCount)}
subtitle={TOUR_SUBTITLE}
/>
);
})}
</>
);
};
FlyoutTour.displayName = 'FlyoutTour';

View file

@ -7,7 +7,6 @@
import { PREFIX } from '../../../shared/test_ids';
export const FLYOUT_TOUR_TEST_ID = `${PREFIX}Tour` as const;
export const FLYOUT_PREVIEW_LINK_TEST_ID = `${PREFIX}PreviewLink` as const;
export const SESSION_VIEW_UPSELL_TEST_ID = `${PREFIX}SessionViewUpsell` as const;

View file

@ -1,199 +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 { EuiText, EuiCode, type EuiTourStepProps } from '@elastic/eui';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { HEADER_NAVIGATION_BUTTON_TEST_ID } from '../../../shared/components/test_ids';
import { OVERVIEW_TAB_LABEL_TEST_ID } from '../../right/test_ids';
import { RULE_SUMMARY_BUTTON_TEST_ID } from '../../right/components/test_ids';
import {
INSIGHTS_TAB_PREVALENCE_BUTTON_LABEL_TEST_ID,
INSIGHTS_TAB_ENTITIES_BUTTON_LABEL_TEST_ID,
} from '../../left/tabs/test_ids';
export const FLYOUT_TOUR_CONFIG_ANCHORS = {
OVERVIEW_TAB: OVERVIEW_TAB_LABEL_TEST_ID,
RULE_PREVIEW: RULE_SUMMARY_BUTTON_TEST_ID,
NAVIGATION_BUTTON: HEADER_NAVIGATION_BUTTON_TEST_ID,
ENTITIES: INSIGHTS_TAB_ENTITIES_BUTTON_LABEL_TEST_ID,
PREVALENCE: INSIGHTS_TAB_PREVALENCE_BUTTON_LABEL_TEST_ID,
};
export interface TourState {
/**
* The current step number
*/
currentTourStep: number;
/**
* True if tour is active (user has not completed or exited the tour)
*/
isTourActive: boolean;
}
export const tourConfig: TourState = {
currentTourStep: 1,
isTourActive: true,
};
export interface FlyoutTourStepsProps {
/**
* Title of the tour step
*/
title: string;
/**
* Content of tour step
*/
content: JSX.Element;
/**
* Step number
*/
stepNumber: number;
/**
* Data test subject of the anchor component
*/
anchor: string;
/**
* Optional anchor position prop
*/
anchorPosition?: EuiTourStepProps['anchorPosition'];
}
export const getRightSectionTourSteps = (): FlyoutTourStepsProps[] => {
const rightSectionTourSteps: FlyoutTourStepsProps[] = [
{
title: i18n.translate('xpack.securitySolution.flyout.tour.overview.title', {
defaultMessage: 'More ways to understand your alerts',
}),
content: (
<EuiText>
<FormattedMessage
id="xpack.securitySolution.flyout.tour.overview.description"
defaultMessage="Explore new insights in the {entities} and {prevalence} sections."
values={{
entities: (
<EuiCode>
{i18n.translate('xpack.securitySolution.flyout.tour.overview.entities.text', {
defaultMessage: 'Entities',
})}
</EuiCode>
),
prevalence: (
<EuiCode>
{i18n.translate('xpack.securitySolution.flyout.tour.overview.prevalence.text', {
defaultMessage: 'Prevalence',
})}
</EuiCode>
),
}}
/>
</EuiText>
),
stepNumber: 1,
anchor: FLYOUT_TOUR_CONFIG_ANCHORS.OVERVIEW_TAB,
anchorPosition: 'downCenter',
},
{
title: i18n.translate('xpack.securitySolution.flyout.tour.preview.title', {
defaultMessage: 'A quick way to access rule details',
}),
content: (
<EuiText>
<FormattedMessage
id="xpack.securitySolution.flyout.tour.rulePreview.description"
defaultMessage="Click {rulePreview} to learn more about the rule that generated the alert."
values={{
rulePreview: (
<EuiCode>
{i18n.translate('xpack.securitySolution.flyout.tour.rulePreview.text', {
defaultMessage: 'Show rule summary',
})}
</EuiCode>
),
}}
/>
</EuiText>
),
stepNumber: 2,
anchor: FLYOUT_TOUR_CONFIG_ANCHORS.RULE_PREVIEW,
anchorPosition: 'rightUp',
},
{
title: i18n.translate('xpack.securitySolution.flyout.tour.expandDetails.title', {
defaultMessage: 'An expanded view of important alert details',
}),
content: (
<EuiText>
<FormattedMessage
id="xpack.securitySolution.flyout.tour.expandDetails.description"
defaultMessage="Click the linked text to open and close the flyout's left panel. The left panel is a detailed view of sections in the right panel."
/>
</EuiText>
),
stepNumber: 3,
anchor: FLYOUT_TOUR_CONFIG_ANCHORS.NAVIGATION_BUTTON,
anchorPosition: 'downCenter',
},
];
return rightSectionTourSteps;
};
export const getLeftSectionTourSteps = (): FlyoutTourStepsProps[] => {
return [
{
title: i18n.translate('xpack.securitySolution.flyout.tour.entities.title', {
defaultMessage: 'New host and user insights are available',
}),
content: (
<EuiText>
<FormattedMessage
id="xpack.securitySolution.flyout.tour.entities.description"
defaultMessage="Check out the expanded {entities} view to learn more about hosts and users that are related to the alert."
values={{
entities: (
<EuiCode>
{i18n.translate('xpack.securitySolution.flyout.tour.entities.text', {
defaultMessage: 'Entities',
})}
</EuiCode>
),
}}
/>
</EuiText>
),
stepNumber: 4,
anchor: FLYOUT_TOUR_CONFIG_ANCHORS.ENTITIES,
anchorPosition: 'rightUp',
},
{
title: i18n.translate('xpack.securitySolution.flyout.tour.prevalence.title', {
defaultMessage: 'New host and user insights are available',
}),
content: (
<EuiText>
<FormattedMessage
id="xpack.securitySolution.flyout.tour.prevalence.description"
defaultMessage="Check out the expanded {prevalence} view to learn how the alert is related to other alerts, events, and entities."
values={{
prevalence: (
<EuiCode>
{i18n.translate('xpack.securitySolution.flyout.tour.prevalence.text', {
defaultMessage: 'Prevalence',
})}
</EuiCode>
),
}}
/>
</EuiText>
),
stepNumber: 5,
anchor: FLYOUT_TOUR_CONFIG_ANCHORS.PREVALENCE,
anchorPosition: 'rightUp',
},
];
};

View file

@ -22,7 +22,6 @@ import {
HEADER_ACTIONS_TEST_ID,
COLLAPSE_DETAILS_BUTTON_TEST_ID,
EXPAND_DETAILS_BUTTON_TEST_ID,
HEADER_NAVIGATION_BUTTON_TEST_ID,
} from './test_ids';
export interface FlyoutNavigationProps {
@ -116,7 +115,7 @@ export const FlyoutNavigation: FC<FlyoutNavigationProps> = memo(
height: ${euiTheme.size.xxl};
`}
>
<EuiFlexItem grow={false} data-test-subj={HEADER_NAVIGATION_BUTTON_TEST_ID}>
<EuiFlexItem grow={false}>
{flyoutIsExpandable && expandDetails && (isExpanded ? collapseButton : expandButton)}
</EuiFlexItem>
{actions && (

View file

@ -27,7 +27,6 @@ export const EXPANDABLE_PANEL_CONTENT_TEST_ID = (dataTestSubj: string) => `${dat
/* Header Navigation */
const FLYOUT_NAVIGATION_TEST_ID = `${PREFIX}Navigation` as const;
export const HEADER_NAVIGATION_BUTTON_TEST_ID = `${PREFIX}NavigationButton` as const;
export const EXPAND_DETAILS_BUTTON_TEST_ID =
`${FLYOUT_NAVIGATION_TEST_ID}ExpandDetailButton` as const;
export const COLLAPSE_DETAILS_BUTTON_TEST_ID =

View file

@ -38809,25 +38809,6 @@
"xpack.securitySolution.flyout.shared.errorTitle": "Impossible d'afficher {title}.",
"xpack.securitySolution.flyout.shared.ExpandablePanelButtonIconAriaLabel": "Activer/Désactiver le panneau extensible",
"xpack.securitySolution.flyout.shared.expandablePanelLoadingAriaLabel": "panneau extensible",
"xpack.securitySolution.flyout.tour.entities.description": "Consultez la vue {entities} étendue pour en savoir plus sur les hôtes et les utilisateurs liés à l'alerte.",
"xpack.securitySolution.flyout.tour.entities.text": "Entités",
"xpack.securitySolution.flyout.tour.entities.title": "De nouvelles informations sur les hôtes et les utilisateurs sont disponibles",
"xpack.securitySolution.flyout.tour.exit.text": "Quitter",
"xpack.securitySolution.flyout.tour.expandDetails.description": "Cliquez sur le texte lié pour ouvrir et fermer le panneau gauche du menu volant. Le panneau de gauche est une vue détaillée des sections du panneau de droite.",
"xpack.securitySolution.flyout.tour.expandDetails.title": "Un affichage élargi des détails importants de l'alerte",
"xpack.securitySolution.flyout.tour.finish.text": "Terminer",
"xpack.securitySolution.flyout.tour.Next.text": "Suivant",
"xpack.securitySolution.flyout.tour.overview.description": "Explorez de nouvelles informations exploitables dans les sections {entities} et {prevalence}.",
"xpack.securitySolution.flyout.tour.overview.entities.text": "Entités",
"xpack.securitySolution.flyout.tour.overview.prevalence.text": "Prévalence",
"xpack.securitySolution.flyout.tour.overview.title": "Plus de moyens pour comprendre vos alertes",
"xpack.securitySolution.flyout.tour.prevalence.description": "Consultez la vue {prevalence} étendue pour savoir comment l'alerte est liée à d'autres alertes, événements et entités.",
"xpack.securitySolution.flyout.tour.prevalence.text": "Prévalence",
"xpack.securitySolution.flyout.tour.prevalence.title": "De nouvelles informations sur les hôtes et les utilisateurs sont disponibles",
"xpack.securitySolution.flyout.tour.preview.title": "Un moyen rapide d'accéder aux détails des règles",
"xpack.securitySolution.flyout.tour.rulePreview.description": "Cliquez sur {rulePreview} pour en savoir plus sur la règle qui a généré l'alerte.",
"xpack.securitySolution.flyout.tour.rulePreview.text": "Afficher le résumé de la règle",
"xpack.securitySolution.flyout.tour.subtitle": "Une expérience d'alerte repensée",
"xpack.securitySolution.flyout.user.preview.viewDetailsLabel": "Ouvrez le menu volant des détails de l'utilisateur",
"xpack.securitySolution.footer.autoRefreshActiveDescription": "Actualisation automatique active",
"xpack.securitySolution.footer.autoRefreshActiveTooltip": "Lorsque l'actualisation automatique est activée, la chronologie vous montrera les {numberOfItems} derniers événements correspondant à votre recherche.",

View file

@ -38552,25 +38552,6 @@
"xpack.securitySolution.flyout.shared.errorTitle": "{title}を表示できません。",
"xpack.securitySolution.flyout.shared.ExpandablePanelButtonIconAriaLabel": "展開可能なパネルトグル",
"xpack.securitySolution.flyout.shared.expandablePanelLoadingAriaLabel": "展開可能なパネル",
"xpack.securitySolution.flyout.tour.entities.description": "アラートに関連付けられたホストとユーザーの詳細については、展開された{entities}ビューを確認してください。",
"xpack.securitySolution.flyout.tour.entities.text": "エンティティ",
"xpack.securitySolution.flyout.tour.entities.title": "新しいホストとユーザーのインサイトがあります",
"xpack.securitySolution.flyout.tour.exit.text": "終了",
"xpack.securitySolution.flyout.tour.expandDetails.description": "リンクされたテキストをクリックすると、フライアウトの左パネルが開いて閉じます。左パネルは、右パネルのセクションの詳細表示です。",
"xpack.securitySolution.flyout.tour.expandDetails.title": "重要なアラート詳細の展開表示",
"xpack.securitySolution.flyout.tour.finish.text": "終了",
"xpack.securitySolution.flyout.tour.Next.text": "次へ",
"xpack.securitySolution.flyout.tour.overview.description": "{entities}および{prevalence}セクションで新しいインサイトを探索してください。",
"xpack.securitySolution.flyout.tour.overview.entities.text": "エンティティ",
"xpack.securitySolution.flyout.tour.overview.prevalence.text": "発生率",
"xpack.securitySolution.flyout.tour.overview.title": "アラートを理解するためのその他の方法",
"xpack.securitySolution.flyout.tour.prevalence.description": "アラートが他のアラート、イベント、エンティティに関連付けられる詳細な方法については、展開された{prevalence}ビューを確認してください。",
"xpack.securitySolution.flyout.tour.prevalence.text": "発生率",
"xpack.securitySolution.flyout.tour.prevalence.title": "新しいホストとユーザーのインサイトがあります",
"xpack.securitySolution.flyout.tour.preview.title": "ルール詳細をすばやく表示する方法",
"xpack.securitySolution.flyout.tour.rulePreview.description": "アラートを生成したルールの詳細については、{rulePreview}をクリックしてください。",
"xpack.securitySolution.flyout.tour.rulePreview.text": "ルール概要を表示",
"xpack.securitySolution.flyout.tour.subtitle": "再設計されたアラート体験",
"xpack.securitySolution.flyout.user.preview.viewDetailsLabel": "ユーザー詳細フライアウトを開く",
"xpack.securitySolution.footer.autoRefreshActiveDescription": "自動更新アクション",
"xpack.securitySolution.footer.autoRefreshActiveTooltip": "自動更新が有効な間、タイムラインはクエリーに一致する最新の {numberOfItems} 件のイベントを表示します。",

View file

@ -6846,7 +6846,6 @@
"securitySolutionPackages.features.featureRegistry.subFeatures.trustedApplications": "受信任的应用程序",
"securitySolutionPackages.features.featureRegistry.subFeatures.trustedApplications.description": "帮助减少与其他软件(通常指其他防病毒或终端安全应用程序)的冲突。",
"securitySolutionPackages.features.featureRegistry.subFeatures.trustedApplications.privilegesTooltip": "访问受信任的应用程序需要所有工作区。",
"sgcuritySolutionPackages.flyout.shared.errorDescription": "显示 {message} 时出现错误。",
"securitySolutionPackages.markdown.insight.upsell": "升级到{requiredLicense}以利用调查指南中的洞见",
"securitySolutionPackages.markdown.investigationGuideInteractions.upsell": "升级到 {requiredLicense} 以利用调查指南交互",
"securitySolutionPackages.navigation.landingLinks": "安全视图",
@ -38598,25 +38597,6 @@
"xpack.securitySolution.flyout.shared.errorTitle": "无法显示 {title}。",
"xpack.securitySolution.flyout.shared.ExpandablePanelButtonIconAriaLabel": "可展开面板切换按钮",
"xpack.securitySolution.flyout.shared.expandablePanelLoadingAriaLabel": "可展开面板",
"xpack.securitySolution.flyout.tour.entities.description": "请查阅展开的 {entities} 视图以了解与该告警有关的主机和用户的更多信息。",
"xpack.securitySolution.flyout.tour.entities.text": "实体",
"xpack.securitySolution.flyout.tour.entities.title": "有新主机和用户洞见可用",
"xpack.securitySolution.flyout.tour.exit.text": "退出",
"xpack.securitySolution.flyout.tour.expandDetails.description": "单击链接文本可打开和关闭浮出控件的左面板。左面板提供了右面板中的各个部分的详细视图。",
"xpack.securitySolution.flyout.tour.expandDetails.title": "重要告警详情的扩展视图",
"xpack.securitySolution.flyout.tour.finish.text": "完成",
"xpack.securitySolution.flyout.tour.Next.text": "下一步",
"xpack.securitySolution.flyout.tour.overview.description": "浏览 {entities} 和 {prevalence} 部分中的新洞见。",
"xpack.securitySolution.flyout.tour.overview.entities.text": "实体",
"xpack.securitySolution.flyout.tour.overview.prevalence.text": "普及率",
"xpack.securitySolution.flyout.tour.overview.title": "了解告警的更多方式",
"xpack.securitySolution.flyout.tour.prevalence.description": "请查阅展开的 {prevalence} 视图以了解该告警与其他告警、事件和实体的关系。",
"xpack.securitySolution.flyout.tour.prevalence.text": "普及率",
"xpack.securitySolution.flyout.tour.prevalence.title": "有新主机和用户洞见可用",
"xpack.securitySolution.flyout.tour.preview.title": "一种快速访问规则详情的方法",
"xpack.securitySolution.flyout.tour.rulePreview.description": "单击 {rulePreview} 以了解与生成该告警的规则有关的详情。",
"xpack.securitySolution.flyout.tour.rulePreview.text": "显示规则摘要",
"xpack.securitySolution.flyout.tour.subtitle": "经过重新设计的告警体验",
"xpack.securitySolution.flyout.user.preview.viewDetailsLabel": "打开用户详情浮出控件",
"xpack.securitySolution.footer.autoRefreshActiveDescription": "自动刷新已启用",
"xpack.securitySolution.footer.autoRefreshActiveTooltip": "自动刷新已启用时,时间线将显示匹配查询的最近 {numberOfItems} 个事件。",