Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution] Repurpose attack discover tour into knowledge base tour #196615

Merged
merged 16 commits into from
Oct 23, 2024
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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}
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