[8.16] [Security Solution] Repurpose attack discover tour into knowledge base tour (#196615) (#197535)

# Backport

This will backport the following commits from `main` to `8.16`:
- [[Security Solution] Repurpose attack discover tour into knowledge
base tour (#196615)](https://github.com/elastic/kibana/pull/196615)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Steph
Milovic","email":"stephanie.milovic@elastic.co"},"sourceCommit":{"committedDate":"2024-10-23T21:02:35Z","message":"[Security
Solution] Repurpose attack discover tour into knowledge base tour
(#196615)","sha":"fa9bb19f14648bbe34493481df0b32838d0e5734","branchLabelMapping":{"^v9.0.0$":"main","^v8.17.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","Team:
SecuritySolution","backport:prev-minor","Team:Security Generative
AI","v8.16.0"],"title":"[Security Solution] Repurpose attack discover
tour into knowledge base
tour","number":196615,"url":"https://github.com/elastic/kibana/pull/196615","mergeCommit":{"message":"[Security
Solution] Repurpose attack discover tour into knowledge base tour
(#196615)","sha":"fa9bb19f14648bbe34493481df0b32838d0e5734"}},"sourceBranch":"main","suggestedTargetBranches":["8.16"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/196615","number":196615,"mergeCommit":{"message":"[Security
Solution] Repurpose attack discover tour into knowledge base tour
(#196615)","sha":"fa9bb19f14648bbe34493481df0b32838d0e5734"}},{"branch":"8.16","label":"v8.16.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Steph Milovic <stephanie.milovic@elastic.co>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2024-10-24 10:47:20 +11:00 committed by GitHub
parent 871137b456
commit a8d1c24dab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 388 additions and 405 deletions

View file

@ -18,6 +18,7 @@ import {
} from '@elastic/eui';
import { css } from '@emotion/react';
import { euiThemeVars } from '@kbn/ui-theme';
import { KnowledgeBaseTour } from '../../../tour/knowledge_base';
import { AnonymizationSettingsManagement } from '../../../data_anonymization/settings/anonymization_settings_management';
import { useAssistantContext } from '../../../..';
import * as i18n from '../../assistant_header/translations';
@ -172,13 +173,15 @@ export const SettingsContextMenu: React.FC<Params> = React.memo(
<>
<EuiPopover
button={
<EuiButtonIcon
aria-label="test"
isDisabled={isDisabled}
iconType="boxesVertical"
onClick={onButtonClick}
data-test-subj="chat-context-menu"
/>
<KnowledgeBaseTour>
<EuiButtonIcon
aria-label="test"
isDisabled={isDisabled}
iconType="boxesVertical"
onClick={onButtonClick}
data-test-subj="chat-context-menu"
/>
</KnowledgeBaseTour>
}
isOpen={isPopoverOpen}
closePopover={closePopover}

View file

@ -30,6 +30,7 @@ import {
} from '@kbn/elastic-assistant-common';
import { css } from '@emotion/react';
import { DataViewsContract } from '@kbn/data-views-plugin/public';
import { KnowledgeBaseTour } from '../../tour/knowledge_base';
import { AlertsSettingsManagement } from '../../assistant/settings/alerts_settings/alerts_settings_management';
import { useKnowledgeBaseEntries } from '../../assistant/api/knowledge_base/entries/use_knowledge_base_entries';
import { useAssistantContext } from '../../assistant_context';
@ -295,7 +296,6 @@ export const KnowledgeBaseSettingsManagement: React.FC<Params> = React.memo(({ d
</>
);
}
return (
<>
<EuiPanel hasShadow={false} hasBorder paddingSize="l">
@ -412,6 +412,7 @@ export const KnowledgeBaseSettingsManagement: React.FC<Params> = React.memo(({ d
<p>{i18n.DELETE_ENTRY_CONFIRMATION_CONTENT}</p>
</EuiConfirmModal>
)}
<KnowledgeBaseTour isKbSettingsPage />
</>
);
});

View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export const NEW_FEATURES_TOUR_STORAGE_KEYS = {
KNOWLEDGE_BASE: 'elasticAssistant.knowledgeBase.newFeaturesTour.v8.16',
};

View file

@ -0,0 +1,142 @@
/*
* 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, screen } from '@testing-library/react';
import { EuiTourStepProps } from '@elastic/eui';
import useLocalStorage from 'react-use/lib/useLocalStorage';
import { KnowledgeBaseTour } from '.';
import { TestProviders } from '../../mock/test_providers/test_providers';
import { useAssistantContext } from '../../..';
jest.mock('../../..');
jest.mock('react-use/lib/useLocalStorage');
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
...original,
EuiTourStep: ({ children, panelProps }: EuiTourStepProps) =>
children ? (
<div data-test-subj={panelProps?.['data-test-subj']}>{children}</div>
) : (
<div data-test-subj={panelProps?.['data-test-subj']} />
),
};
});
describe('Attack discovery tour', () => {
const persistToLocalStorage = jest.fn();
const navigateToApp = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
(useAssistantContext as jest.Mock).mockReturnValue({
navigateToApp,
assistantFeatures: {
assistantKnowledgeBaseByDefault: true,
},
});
jest.mocked(useLocalStorage).mockReturnValue([
{
currentTourStep: 1,
isTourActive: true,
},
persistToLocalStorage,
] as unknown as ReturnType<typeof useLocalStorage>);
});
it('should not render any tour steps when tour is not activated', () => {
jest.mocked(useLocalStorage).mockReturnValue([
{
currentTourStep: 1,
isTourActive: false,
},
persistToLocalStorage,
] as unknown as ReturnType<typeof useLocalStorage>);
render(
<KnowledgeBaseTour>
<h1>{'Hello world'}</h1>
</KnowledgeBaseTour>,
{
wrapper: TestProviders,
}
);
expect(screen.queryByTestId('knowledgeBase-tour-step-1')).toBeNull();
expect(screen.queryByTestId('knowledgeBase-tour-step-2')).toBeNull();
});
it('should not render any tour steps when knowledge base feature flag is not activated', () => {
(useAssistantContext as jest.Mock).mockReturnValue({
navigateToApp,
assistantFeatures: {
assistantKnowledgeBaseByDefault: false,
},
});
render(
<KnowledgeBaseTour>
<h1>{'Hello world'}</h1>
</KnowledgeBaseTour>,
{
wrapper: TestProviders,
}
);
expect(screen.queryByTestId('knowledgeBase-tour-step-1')).toBeNull();
expect(screen.queryByTestId('knowledgeBase-tour-step-2')).toBeNull();
});
it('should not render any tour steps when tour is on step 2 and page is not knowledge base', () => {
jest.mocked(useLocalStorage).mockReturnValue([
{
currentTourStep: 2,
isTourActive: true,
},
persistToLocalStorage,
] as unknown as ReturnType<typeof useLocalStorage>);
render(
<KnowledgeBaseTour>
<h1>{'Hello world'}</h1>
</KnowledgeBaseTour>,
{
wrapper: TestProviders,
}
);
expect(screen.queryByTestId('knowledgeBase-tour-step-1')).toBeNull();
});
it('should render tour step 1 when element is mounted', async () => {
const { getByTestId } = render(
<KnowledgeBaseTour>
<h1>{'Hello world'}</h1>
</KnowledgeBaseTour>,
{
wrapper: TestProviders,
}
);
expect(getByTestId('knowledgeBase-tour-step-1')).toBeInTheDocument();
});
it('should render tour video when tour is on step 2 and page is knowledge base', () => {
jest.mocked(useLocalStorage).mockReturnValue([
{
currentTourStep: 2,
isTourActive: true,
},
persistToLocalStorage,
] as unknown as ReturnType<typeof useLocalStorage>);
const { getByTestId } = render(<KnowledgeBaseTour isKbSettingsPage />, {
wrapper: TestProviders,
});
expect(screen.queryByTestId('knowledgeBase-tour-step-1')).toBeNull();
expect(getByTestId('knowledgeBase-tour-step-2')).toBeInTheDocument();
});
it('should advance to tour step 2 when page is knowledge base', () => {
render(<KnowledgeBaseTour isKbSettingsPage />, { wrapper: TestProviders });
const nextStep = persistToLocalStorage.mock.calls[0][0];
expect(nextStep()).toEqual({ isTourActive: true, currentTourStep: 2 });
});
});

View file

@ -0,0 +1,137 @@
/*
* 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.
*/
/*
* The knowledge base tour for 8.14
*
* */
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { EuiButton, EuiButtonEmpty, EuiTourStep, EuiTourStepProps } from '@elastic/eui';
import useLocalStorage from 'react-use/lib/useLocalStorage';
import { KNOWLEDGE_BASE_TAB } from '../../assistant/settings/const';
import { useAssistantContext } from '../../..';
import { VideoToast } from './video_toast';
import { NEW_FEATURES_TOUR_STORAGE_KEYS } from '../const';
import { knowledgeBaseTourStepOne, tourConfig } from './step_config';
import * as i18n from './translations';
interface TourState {
currentTourStep: number;
isTourActive: boolean;
}
const KnowledgeBaseTourComp: React.FC<{
children?: EuiTourStepProps['children'];
isKbSettingsPage?: boolean;
}> = ({ children, isKbSettingsPage = false }) => {
const {
navigateToApp,
assistantFeatures: { assistantKnowledgeBaseByDefault: enableKnowledgeBaseByDefault },
} = useAssistantContext();
const [tourState, setTourState] = useLocalStorage<TourState>(
NEW_FEATURES_TOUR_STORAGE_KEYS.KNOWLEDGE_BASE,
tourConfig
);
const advanceToVideoStep = useCallback(
() =>
setTourState((prev = tourConfig) => ({
...prev,
currentTourStep: 2,
})),
[setTourState]
);
useEffect(() => {
if (tourState?.isTourActive && isKbSettingsPage) {
advanceToVideoStep();
}
}, [advanceToVideoStep, isKbSettingsPage, tourState?.isTourActive]);
const finishTour = useCallback(
() =>
setTourState((prev = tourConfig) => ({
...prev,
isTourActive: false,
})),
[setTourState]
);
const navigateToKnowledgeBase = useCallback(
() =>
navigateToApp('management', {
path: `kibana/securityAiAssistantManagement?tab=${KNOWLEDGE_BASE_TAB}`,
}),
[navigateToApp]
);
const nextStep = useCallback(() => {
if (tourState?.currentTourStep === 1) {
navigateToKnowledgeBase();
advanceToVideoStep();
}
}, [tourState?.currentTourStep, navigateToKnowledgeBase, advanceToVideoStep]);
const footerAction = useMemo(
() => [
// if exit, set tour to the video step without navigating to the page
<EuiButtonEmpty size="s" color="text" onClick={advanceToVideoStep}>
{i18n.KNOWLEDGE_BASE_TOUR_EXIT}
</EuiButtonEmpty>,
// if next, set tour to the video step and navigate to the page
<EuiButton color="success" size="s" onClick={nextStep}>
{i18n.KNOWLEDGE_BASE_TRY_IT}
</EuiButton>,
],
[advanceToVideoStep, nextStep]
);
const isTestAutomation =
// @ts-ignore
window.Cypress != null || // TODO: temporary workaround to disable the tour when running in Cypress, because the tour breaks other projects Cypress tests
navigator.webdriver === true; // TODO: temporary workaround to disable the tour when running in the FTR, because the tour breaks other projects FTR tests
const [isTimerExhausted, setIsTimerExhausted] = useState(false);
useEffect(() => {
const timer = setTimeout(() => {
setIsTimerExhausted(true);
}, 1000);
return () => clearTimeout(timer);
}, []);
if (!enableKnowledgeBaseByDefault || isTestAutomation || !tourState?.isTourActive) {
return children ?? null;
}
return tourState?.currentTourStep === 1 && children ? (
<EuiTourStep
anchorPosition={'downRight'}
content={knowledgeBaseTourStepOne.content}
footerAction={footerAction}
isStepOpen={isTimerExhausted}
maxWidth={450}
onFinish={advanceToVideoStep}
panelProps={{
'data-test-subj': `knowledgeBase-tour-step-1`,
}}
step={1}
stepsTotal={1}
title={knowledgeBaseTourStepOne.title}
>
{children}
</EuiTourStep>
) : isKbSettingsPage ? (
<VideoToast onClose={finishTour} />
) : (
children ?? null
);
};
export const KnowledgeBaseTour = React.memo(KnowledgeBaseTourComp);

View file

@ -7,14 +7,9 @@
import * as i18n from './translations';
export const ATTACK_DISCOVERY_TOUR_CONFIG_ANCHORS = {
NAV_LINK: 'solutionSideNavItemLink-attack_discovery',
};
export const attackDiscoveryTourStepOne = {
title: i18n.ATTACK_DISCOVERY_TOUR_ATTACK_DISCOVERY_TITLE,
content: i18n.ATTACK_DISCOVERY_TOUR_ATTACK_DISCOVERY_DESC,
anchor: ATTACK_DISCOVERY_TOUR_CONFIG_ANCHORS.NAV_LINK,
export const knowledgeBaseTourStepOne = {
title: i18n.KNOWLEDGE_BASE_TOUR_KNOWLEDGE_BASE_TITLE,
content: i18n.KNOWLEDGE_BASE_TOUR_KNOWLEDGE_BASE_DESC,
};
export const tourConfig = {

View file

@ -0,0 +1,59 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
export const KNOWLEDGE_BASE_TOUR_KNOWLEDGE_BASE_TITLE = i18n.translate(
'xpack.elasticAssistant.knowledgeBase.tour.navStep.title',
{
defaultMessage: 'New: Custom Knowledge Sources',
}
);
export const KNOWLEDGE_BASE_TOUR_KNOWLEDGE_BASE_DESC = i18n.translate(
'xpack.elasticAssistant.knowledgeBase.tour.navStep.desc',
{
defaultMessage:
'Access the new settings menu to add custom data sources for use within the AI Assistant.',
}
);
export const KNOWLEDGE_BASE_TOUR_VIDEO_STEP_TITLE = i18n.translate(
'xpack.elasticAssistant.knowledgeBase.tour.videoStep.title',
{
defaultMessage: 'Introducing Custom Knowledge Sources',
}
);
export const KNOWLEDGE_BASE_TOUR_VIDEO_STEP_DESC = i18n.translate(
'xpack.elasticAssistant.knowledgeBase.tour.videoStep.desc',
{
defaultMessage:
'Custom knowledge sources enable you to receive bespoke, tailored responses from the AI Assistant. Watch this video to learn how to add your own data sources and explore examples of how they can be applied in a security operations context.',
}
);
export const KNOWLEDGE_BASE_TOUR_EXIT = i18n.translate(
'xpack.elasticAssistant.knowledgeBase.tour.exit',
{
defaultMessage: 'Close',
}
);
export const KNOWLEDGE_BASE_TRY_IT = i18n.translate(
'xpack.elasticAssistant.knowledgeBase.tour.tryIt',
{
defaultMessage: 'Try it',
}
);
export const WATCH_OVERVIEW_VIDEO = i18n.translate(
'xpack.elasticAssistant.knowledgeBase.tour.video',
{
defaultMessage: 'Watch overview video',
}
);

View file

@ -20,7 +20,7 @@ describe('VideoToast', () => {
jest.restoreAllMocks();
});
it('should render the video toast', () => {
const videoToast = screen.getByTestId('attackDiscovery-tour-step-2');
const videoToast = screen.getByTestId('knowledgeBase-tour-step-2');
expect(videoToast).toBeInTheDocument();
});

View file

@ -19,6 +19,8 @@ import * as i18n from './translations';
import theGif from './overview.gif';
const VIDEO_CONTENT_WIDTH = 250;
// TODO before removing assistantKnowledgeBaseByDefault feature flag
// update the VIDEO_PAGE to the correct URL
const VIDEO_PAGE = `https://videos.elastic.co/watch/BrDaDBAAvdygvemFKNAkBW`;
const VideoComponent: React.FC<{ onClose: () => void }> = ({ onClose }) => {
@ -28,11 +30,17 @@ const VideoComponent: React.FC<{ onClose: () => void }> = ({ onClose }) => {
return (
<EuiPortal>
<div
data-test-subj="attackDiscovery-tour-step-2"
css={{ position: 'fixed', bottom: 16, right: 16, zIndex: 9999 }}
>
<EuiToast onClose={onClose} css={{ maxWidth: VIDEO_CONTENT_WIDTH }}>
<div data-test-subj="knowledgeBase-tour-step-2">
<EuiToast
onClose={onClose}
css={{
maxWidth: VIDEO_CONTENT_WIDTH,
position: 'fixed',
bottom: 16,
right: 16,
zIndex: 9999,
}}
>
<EuiImage
onClick={openVideoInNewTab}
css={{ marginTop: 20, '&:hover': { cursor: 'pointer' } }}
@ -42,9 +50,9 @@ const VideoComponent: React.FC<{ onClose: () => void }> = ({ onClose }) => {
/>
<EuiText size="s" grow={false} css={{ marginTop: 20 }}>
<h4>
<EuiIcon type="cheer" color="success" /> {i18n.ATTACK_DISCOVERY_TOUR_VIDEO_STEP_TITLE}
<EuiIcon type="cheer" color="success" /> {i18n.KNOWLEDGE_BASE_TOUR_VIDEO_STEP_TITLE}
</h4>
<p>{i18n.ATTACK_DISCOVERY_TOUR_VIDEO_STEP_DESC}</p>
<p>{i18n.KNOWLEDGE_BASE_TOUR_VIDEO_STEP_DESC}</p>
</EuiText>
<EuiSpacer size="m" />
<EuiButton color="success" onClick={openVideoInNewTab} fullWidth>

View file

@ -5,7 +5,8 @@
"types": [
"jest",
"node",
"react"
"react",
"@kbn/ambient-ui-types"
]
},
"include": [

View file

@ -167,11 +167,5 @@ describe('SolutionSideNav', () => {
expect(result.queryByTestId('solutionSideNavPanel')).toBeInTheDocument();
expect(result.getByText('Users')).toBeInTheDocument();
});
it('should call onMount when function is provided', () => {
const onMount = jest.fn();
renderNav({ onMount });
expect(onMount).toHaveBeenCalled();
});
});
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import {
EuiListGroup,
EuiFlexGroup,
@ -50,7 +50,6 @@ export interface SolutionSideNavProps {
* e.g.: usageCollection?.reportUiCounter?.bind(null, appId)
* */
tracker?: Tracker;
onMount?: () => void;
}
type ActivePanelNav = string | null;
/**
@ -63,7 +62,6 @@ export const SolutionSideNav: React.FC<SolutionSideNavProps> = React.memo(functi
panelBottomOffset,
panelTopOffset,
tracker,
onMount,
}) {
const isMobileSize = useIsWithinBreakpoints(['xs', 's']);
@ -75,10 +73,6 @@ export const SolutionSideNav: React.FC<SolutionSideNavProps> = React.memo(functi
setActivePanelNavId(id);
};
useEffect(() => {
if (onMount) onMount();
}, [onMount]);
const onClosePanelNav = useCallback(() => {
activePanelNavIdRef.current = null;
setActivePanelNavId(null);

View file

@ -47,7 +47,7 @@ export const NEW_FEATURES_TOUR_STORAGE_KEYS = {
TIMELINES: 'securitySolution.security.timelineFlyoutHeader.saveTimelineTour',
TIMELINE: 'securitySolution.timeline.newFeaturesTour.v8.12',
FLYOUT: 'securitySolution.documentDetails.newFeaturesTour.v8.14',
ATTACK_DISCOVERY: 'securitySolution.attackDiscovery.newFeaturesTour.v8.14',
KNOWLEDGE_BASE: 'elasticAssistant.knowledgeBase.newFeaturesTour.v8.16',
};
/**

View file

@ -424,7 +424,6 @@ export const NEW_FEATURES_TOUR_STORAGE_KEYS = {
TIMELINES: 'securitySolution.security.timelineFlyoutHeader.saveTimelineTour',
TIMELINE: 'securitySolution.timeline.newFeaturesTour.v8.12',
FLYOUT: 'securitySolution.documentDetails.newFeaturesTour.v8.14',
ATTACK_DISCOVERY: 'securitySolution.attackDiscovery.newFeaturesTour.v8.14',
};
export const RULE_DETAILS_EXECUTION_LOG_TABLE_SHOW_METRIC_COLUMNS_STORAGE_KEY =

View file

@ -5,14 +5,13 @@
* 2.0.
*/
import React, { type ReactNode, useMemo, useState, useCallback } from 'react';
import React, { type ReactNode, useMemo } from 'react';
import styled from 'styled-components';
import { EuiThemeProvider, useEuiTheme, type EuiThemeComputed } from '@elastic/eui';
import { IS_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid';
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
import type { KibanaPageTemplateProps } from '@kbn/shared-ux-page-kibana-template';
import { ExpandableFlyoutProvider } from '@kbn/expandable-flyout';
import { AttackDiscoveryTour } from '../../../attack_discovery/tour';
import { URL_PARAM_KEY } from '../../../common/hooks/use_url_state';
import { SecuritySolutionFlyout, TimelineFlyout } from '../../../flyout';
import { useSecuritySolutionNavigation } from '../../../common/components/navigation/use_security_solution_navigation';
@ -56,11 +55,7 @@ export type SecuritySolutionTemplateWrapperProps = Omit<KibanaPageTemplateProps,
export const SecuritySolutionTemplateWrapper: React.FC<SecuritySolutionTemplateWrapperProps> =
React.memo(({ children, ...rest }) => {
const [didMount, setDidMount] = useState(false);
const onMount = useCallback(() => {
setDidMount(true);
}, []);
const solutionNavProps = useSecuritySolutionNavigation(onMount);
const solutionNavProps = useSecuritySolutionNavigation();
const [isTimelineBottomBarVisible] = useShowTimeline();
const getTimelineShowStatus = useMemo(() => getTimelineShowStatusByIdSelector(), []);
const { show: isShowingTimelineOverlay } = useDeepEqualSelector((state) =>
@ -107,8 +102,6 @@ export const SecuritySolutionTemplateWrapper: React.FC<SecuritySolutionTemplateW
{children}
<SecuritySolutionFlyout />
</ExpandableFlyoutProvider>
{didMount && <AttackDiscoveryTour />}
</KibanaPageTemplate.Section>
{isTimelineBottomBarVisible && (
<KibanaPageTemplate.BottomBar data-test-subj="timeline-bottom-bar-container">

View file

@ -1,137 +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 { useIsElementMounted } from '../../detection_engine/rule_management_ui/components/rules_table/rules_table/guided_onboarding/use_is_element_mounted';
import { render, screen } 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 { AttackDiscoveryTour } from '.';
import { ATTACK_DISCOVERY_TOUR_CONFIG_ANCHORS } from './step_config';
import { NEW_FEATURES_TOUR_STORAGE_KEYS, SecurityPageName } from '../../../common/constants';
import type { RouteSpyState } from '../../common/utils/route/types';
import { useRouteSpy } from '../../common/utils/route/use_route_spy';
const mockRouteSpy: RouteSpyState = {
pageName: SecurityPageName.overview,
detailName: undefined,
tabName: undefined,
search: '',
pathName: '/',
};
jest.mock(
'../../detection_engine/rule_management_ui/components/rules_table/rules_table/guided_onboarding/use_is_element_mounted'
);
jest.mock('../../common/lib/kibana');
jest.mock('../../common/utils/route/use_route_spy');
jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
...original,
EuiTourStep: () => <div data-test-subj="attackDiscovery-tour-step-1" />,
};
});
const mockedUseKibana = mockUseKibana();
const { storage: storageMock } = createSecuritySolutionStorageMock();
const mockStore = createMockStore(undefined, undefined, undefined, storageMock);
const TestComponent = () => {
return (
<TestProviders store={mockStore}>
<div id={ATTACK_DISCOVERY_TOUR_CONFIG_ANCHORS.NAV_LINK} />
<AttackDiscoveryTour />
</TestProviders>
);
};
describe('Attack discovery tour', () => {
beforeAll(() => {
(useIsElementMounted as jest.Mock).mockReturnValue(true);
(useRouteSpy as jest.Mock).mockReturnValue([mockRouteSpy]);
});
beforeEach(() => {
(useKibana as jest.Mock).mockReturnValue({
...mockedUseKibana,
services: {
...mockedUseKibana.services,
storage: storageMock,
},
});
storageMock.clear();
});
afterEach(() => {
jest.clearAllMocks();
});
it('should not render tour step 1 when element is not mounted', () => {
(useIsElementMounted as jest.Mock).mockReturnValueOnce(false);
render(<TestComponent />);
expect(screen.queryByTestId('attackDiscovery-tour-step-1')).toBeNull();
});
it('should not render any tour steps when tour is not activated', () => {
storageMock.set(NEW_FEATURES_TOUR_STORAGE_KEYS.ATTACK_DISCOVERY, {
currentTourStep: 1,
isTourActive: false,
});
render(<TestComponent />);
expect(screen.queryByTestId('attackDiscovery-tour-step-1')).toBeNull();
expect(screen.queryByTestId('attackDiscovery-tour-step-2')).toBeNull();
});
it('should not render any tour steps when tour is on step 2 and page is not attack discovery', () => {
storageMock.set(NEW_FEATURES_TOUR_STORAGE_KEYS.ATTACK_DISCOVERY, {
currentTourStep: 2,
isTourActive: true,
});
const { debug } = render(<TestComponent />);
expect(screen.queryByTestId('attackDiscovery-tour-step-1')).toBeNull();
debug();
});
it('should render tour step 1 when element is mounted', async () => {
const { getByTestId } = render(<TestComponent />);
expect(getByTestId('attackDiscovery-tour-step-1')).toBeInTheDocument();
});
it('should render tour video when tour is on step 2 and page is attack discovery', () => {
(useRouteSpy as jest.Mock).mockReturnValue([
{ ...mockRouteSpy, pageName: SecurityPageName.attackDiscovery },
]);
storageMock.set(NEW_FEATURES_TOUR_STORAGE_KEYS.ATTACK_DISCOVERY, {
currentTourStep: 2,
isTourActive: true,
});
const { getByTestId } = render(<TestComponent />);
expect(screen.queryByTestId('attackDiscovery-tour-step-1')).toBeNull();
expect(getByTestId('attackDiscovery-tour-step-2')).toBeInTheDocument();
});
it('should advance to tour step 2 when page is attack discovery', () => {
(useRouteSpy as jest.Mock).mockReturnValue([
{ ...mockRouteSpy, pageName: SecurityPageName.attackDiscovery },
]);
storageMock.set(NEW_FEATURES_TOUR_STORAGE_KEYS.ATTACK_DISCOVERY, {
currentTourStep: 1,
isTourActive: true,
});
render(<TestComponent />);
expect(
storageMock.get(NEW_FEATURES_TOUR_STORAGE_KEYS.ATTACK_DISCOVERY).currentTourStep
).toEqual(2);
});
});

View file

@ -1,133 +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.
*/
/*
* The attack discovery tour for 8.14
*
* */
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { EuiButton, EuiButtonEmpty, EuiTourStep } from '@elastic/eui';
import { useRouteSpy } from '../../common/utils/route/use_route_spy';
import { VideoToast } from './video_toast';
import { useIsElementMounted } from '../../detection_engine/rule_management_ui/components/rules_table/rules_table/guided_onboarding/use_is_element_mounted';
import { NEW_FEATURES_TOUR_STORAGE_KEYS, SecurityPageName } from '../../../common/constants';
import { useKibana, useNavigation } from '../../common/lib/kibana';
import { attackDiscoveryTourStepOne, tourConfig } from './step_config';
import * as i18n from './translations';
interface TourState {
currentTourStep: number;
isTourActive: boolean;
}
const AttackDiscoveryTourComp = () => {
const {
services: { storage },
} = useKibana();
const { navigateTo } = useNavigation();
const [{ pageName }] = useRouteSpy();
const [tourState, setTourState] = useState<TourState>(
storage.get(NEW_FEATURES_TOUR_STORAGE_KEYS.ATTACK_DISCOVERY) ?? tourConfig
);
const advanceToVideoStep = useCallback(() => {
setTourState((prev) => {
storage.set(NEW_FEATURES_TOUR_STORAGE_KEYS.ATTACK_DISCOVERY, {
...prev,
currentTourStep: 2,
});
return {
...prev,
currentTourStep: 2,
};
});
}, [storage]);
useEffect(() => {
if (tourState.isTourActive && pageName === SecurityPageName.attackDiscovery) {
advanceToVideoStep();
}
}, [advanceToVideoStep, pageName, tourState.isTourActive]);
const finishTour = useCallback(() => {
setTourState((prev) => {
storage.set(NEW_FEATURES_TOUR_STORAGE_KEYS.ATTACK_DISCOVERY, {
...prev,
isTourActive: false,
});
return {
...prev,
isTourActive: false,
};
});
}, [storage]);
const navigateToAttackDiscovery = useCallback(() => {
navigateTo({
deepLinkId: SecurityPageName.attackDiscovery,
});
}, [navigateTo]);
const nextStep = useCallback(() => {
if (tourState.currentTourStep === 1) {
navigateToAttackDiscovery();
advanceToVideoStep();
}
}, [tourState.currentTourStep, navigateToAttackDiscovery, advanceToVideoStep]);
const footerAction = useMemo(
() => [
// if exit, set tour to the video step without navigating to the page
<EuiButtonEmpty size="s" color="text" onClick={advanceToVideoStep}>
{i18n.ATTACK_DISCOVERY_TOUR_EXIT}
</EuiButtonEmpty>,
// if next, set tour to the video step and navigate to the page
<EuiButton color="success" size="s" onClick={nextStep}>
{i18n.ATTACK_DISCOVERY_TRY_IT}
</EuiButton>,
],
[advanceToVideoStep, nextStep]
);
const isElementAtCurrentStepMounted = useIsElementMounted(attackDiscoveryTourStepOne?.anchor);
const isTestAutomation =
window.Cypress != null || // TODO: temporary workaround to disable the tour when running in Cypress, because the tour breaks other projects Cypress tests
navigator.webdriver === true; // TODO: temporary workaround to disable the tour when running in the FTR, because the tour breaks other projects FTR tests
if (
isTestAutomation ||
!tourState.isTourActive ||
(tourState.currentTourStep === 1 && !isElementAtCurrentStepMounted)
) {
return null;
}
return tourState.currentTourStep === 1 ? (
<EuiTourStep
anchor={`#${attackDiscoveryTourStepOne.anchor}`}
content={attackDiscoveryTourStepOne.content}
footerAction={footerAction}
isStepOpen
maxWidth={450}
onFinish={advanceToVideoStep}
panelProps={{
'data-test-subj': `attackDiscovery-tour-step-1`,
}}
repositionOnScroll
step={1}
stepsTotal={1}
title={attackDiscoveryTourStepOne.title}
/>
) : pageName === SecurityPageName.attackDiscovery ? (
<VideoToast onClose={finishTour} />
) : null;
};
export const AttackDiscoveryTour = React.memo(AttackDiscoveryTourComp);

View file

@ -1,59 +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 { i18n } from '@kbn/i18n';
export const ATTACK_DISCOVERY_TOUR_ATTACK_DISCOVERY_TITLE = i18n.translate(
'xpack.securitySolution.attackDiscovery.tour.navStep.title',
{
defaultMessage: 'Introducing attack discovery',
}
);
export const ATTACK_DISCOVERY_TOUR_ATTACK_DISCOVERY_DESC = i18n.translate(
'xpack.securitySolution.attackDiscovery.tour.navStep.desc',
{
defaultMessage:
'Leverage Generative AI to find relationships among your alerts and describe attack chains.',
}
);
export const ATTACK_DISCOVERY_TOUR_VIDEO_STEP_TITLE = i18n.translate(
'xpack.securitySolution.attackDiscovery.tour.videoStep.title',
{
defaultMessage: 'Start discovering attacks',
}
);
export const ATTACK_DISCOVERY_TOUR_VIDEO_STEP_DESC = i18n.translate(
'xpack.securitySolution.attackDiscovery.tour.videoStep.desc',
{
defaultMessage:
'Dive into data-driven attack discoveries and streamline your workflow with our intuitive AI technology, designed to elevate your productivity instantly.',
}
);
export const ATTACK_DISCOVERY_TOUR_EXIT = i18n.translate(
'xpack.securitySolution.attackDiscovery.tour.exit',
{
defaultMessage: 'Close',
}
);
export const ATTACK_DISCOVERY_TRY_IT = i18n.translate(
'xpack.securitySolution.attackDiscovery.tour.tryIt',
{
defaultMessage: 'Try it',
}
);
export const WATCH_OVERVIEW_VIDEO = i18n.translate(
'xpack.securitySolution.attackDiscovery.tour.video',
{
defaultMessage: 'Watch overview video',
}
);

View file

@ -137,7 +137,7 @@ const usePanelBottomOffset = (): string | undefined => {
* Main security navigation component.
* It takes the links to render from the generic application `links` configs.
*/
export const SecuritySideNav: React.FC<{ onMount?: () => void }> = ({ onMount }) => {
export const SecuritySideNav: React.FC = () => {
const items = useSolutionSideNavItems();
const selectedId = useSelectedId();
const panelTopOffset = usePanelTopOffset();
@ -151,7 +151,6 @@ export const SecuritySideNav: React.FC<{ onMount?: () => void }> = ({ onMount })
<SolutionSideNav
items={items}
categories={CATEGORIES}
onMount={onMount}
selectedId={selectedId}
panelTopOffset={panelTopOffset}
panelBottomOffset={panelBottomOffset}

View file

@ -23,9 +23,7 @@ const translatedNavTitle = i18n.translate('xpack.securitySolution.navigation.mai
defaultMessage: 'Security',
});
export const useSecuritySolutionNavigation = (
onMount: () => void
): KibanaPageTemplateProps['solutionNav'] => {
export const useSecuritySolutionNavigation = (): KibanaPageTemplateProps['solutionNav'] => {
const { chrome } = useKibana().services;
const chromeStyle$ = useMemo(() => chrome.getChromeStyle$(), [chrome]);
const chromeStyle = useObservable(chromeStyle$, 'classic');
@ -41,7 +39,7 @@ export const useSecuritySolutionNavigation = (
canBeCollapsed: true,
name: translatedNavTitle,
icon: 'logoSecurity',
children: <SecuritySideNav onMount={onMount} />,
children: <SecuritySideNav />,
closeFlyoutButtonPosition: 'inside',
};
};

View file

@ -35336,13 +35336,6 @@
"xpack.securitySolution.attackDiscovery.summaryCount.alertsLabel": "{alertsCount} {alertsCount, plural, =1 {alerte} other {alertes}}",
"xpack.securitySolution.attackDiscovery.summaryCount.discoveriesLabel": "{attackDiscoveriesCount} {attackDiscoveriesCount, plural, =1 {découverte} other {découvertes}}",
"xpack.securitySolution.attackDiscovery.summaryCount.lastGeneratedLabel": "Généré",
"xpack.securitySolution.attackDiscovery.tour.exit": "Fermer",
"xpack.securitySolution.attackDiscovery.tour.navStep.desc": "Tirez parti de lIA générative pour trouver des relations entre vos alertes et détailler les chaînes dattaque.",
"xpack.securitySolution.attackDiscovery.tour.navStep.title": "Présentation dAttack Discovery",
"xpack.securitySolution.attackDiscovery.tour.tryIt": "Essayer",
"xpack.securitySolution.attackDiscovery.tour.video": "Regardez la vidéo de présentation",
"xpack.securitySolution.attackDiscovery.tour.videoStep.desc": "Plongez dans les découvertes d'attaques axées sur les données et rationalisez votre flux de travail grâce à notre technologie d'IA intuitive, conçue pour accroître instantanément votre productivité.",
"xpack.securitySolution.attackDiscovery.tour.videoStep.title": "Démarrez la découverte des attaques",
"xpack.securitySolution.auditd.abortedAuditStartupDescription": "démarrage de l'audit abandonné",
"xpack.securitySolution.auditd.accessErrorDescription": "erreur d'accès",
"xpack.securitySolution.auditd.accessPermissionDescription": "autorisation d'accès",

View file

@ -35081,13 +35081,6 @@
"xpack.securitySolution.attackDiscovery.summaryCount.alertsLabel": "{alertsCount} {alertsCount, plural, other {件のアラート}}",
"xpack.securitySolution.attackDiscovery.summaryCount.discoveriesLabel": "{attackDiscoveriesCount} {attackDiscoveriesCount, plural, other {件の検出}}",
"xpack.securitySolution.attackDiscovery.summaryCount.lastGeneratedLabel": "生成済み",
"xpack.securitySolution.attackDiscovery.tour.exit": "閉じる",
"xpack.securitySolution.attackDiscovery.tour.navStep.desc": "生成AIを活用して、アラート全体の関係を特定し、攻撃チェーンを解析します。",
"xpack.securitySolution.attackDiscovery.tour.navStep.title": "Attack Discoveryの概要",
"xpack.securitySolution.attackDiscovery.tour.tryIt": "お試しください",
"xpack.securitySolution.attackDiscovery.tour.video": "概要動画を視聴",
"xpack.securitySolution.attackDiscovery.tour.videoStep.desc": "データ主導のAttack Discoveryを導入し、生産性を即時に高めるために設計されたElasticの直感的なAI技術でワークフローを合理化しましょう。",
"xpack.securitySolution.attackDiscovery.tour.videoStep.title": "攻撃の検出を開始",
"xpack.securitySolution.auditd.abortedAuditStartupDescription": "中断された監査のスタートアップ",
"xpack.securitySolution.auditd.accessErrorDescription": "アクセスエラー",
"xpack.securitySolution.auditd.accessPermissionDescription": "アクセス権限",

View file

@ -35124,13 +35124,6 @@
"xpack.securitySolution.attackDiscovery.summaryCount.alertsLabel": "{alertsCount} 个{alertsCount, plural, other {告警}}",
"xpack.securitySolution.attackDiscovery.summaryCount.discoveriesLabel": "{attackDiscoveriesCount} 个{attackDiscoveriesCount, plural, other {发现}}",
"xpack.securitySolution.attackDiscovery.summaryCount.lastGeneratedLabel": "已生成",
"xpack.securitySolution.attackDiscovery.tour.exit": "关闭",
"xpack.securitySolution.attackDiscovery.tour.navStep.desc": "利用生成式 AI 找出您的告警之间的关系并描述攻击链。",
"xpack.securitySolution.attackDiscovery.tour.navStep.title": "Attack Discovery 简介",
"xpack.securitySolution.attackDiscovery.tour.tryIt": "试用",
"xpack.securitySolution.attackDiscovery.tour.video": "观看概述视频",
"xpack.securitySolution.attackDiscovery.tour.videoStep.desc": "深入了解数据驱动式 Attack Discovery并利用旨在即时提高生产力的直观式 AI 技术精简您的工作流。",
"xpack.securitySolution.attackDiscovery.tour.videoStep.title": "开始发现攻击",
"xpack.securitySolution.auditd.abortedAuditStartupDescription": "已中止审计启动",
"xpack.securitySolution.auditd.accessErrorDescription": "访问错误",
"xpack.securitySolution.auditd.accessPermissionDescription": "访问权限",