Skip to content

Commit

Permalink
[8.x] [Security Solution] Repurpose attack discover tour into knowled…
Browse files Browse the repository at this point in the history
…ge base tour (#196615) (#197536)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Security Solution] Repurpose attack discover tour into knowledge
base tour (#196615)](#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":"[email protected]"},"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 <[email protected]>
  • Loading branch information
kibanamachine and stephmilovic authored Oct 23, 2024
1 parent 9a0500d commit 7068760
Show file tree
Hide file tree
Showing 24 changed files with 388 additions and 405 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,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';
Expand Down Expand Up @@ -189,13 +190,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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -295,7 +296,6 @@ export const KnowledgeBaseSettingsManagement: React.FC<Params> = React.memo(({ d
</>
);
}

return (
<>
<EuiPanel hasShadow={false} hasBorder paddingSize="l">
Expand Down Expand Up @@ -412,6 +412,7 @@ export const KnowledgeBaseSettingsManagement: React.FC<Params> = React.memo(({ d
<p>{i18n.DELETE_ENTRY_CONFIRMATION_CONTENT}</p>
</EuiConfirmModal>
)}
<KnowledgeBaseTour isKbSettingsPage />
</>
);
});
Expand Down
10 changes: 10 additions & 0 deletions x-pack/packages/kbn-elastic-assistant/impl/tour/const.ts
Original file line number Diff line number Diff line change
@@ -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',
};
Original file line number Diff line number Diff line change
@@ -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 });
});
});
Original file line number Diff line number Diff line change
@@ -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);
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Loading

0 comments on commit 7068760

Please sign in to comment.