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 @@ -167,18 +168,22 @@ export const SettingsContextMenu: React.FC<Params> = React.memo(
closeDestroyModal();
closePopover?.();
}, [onChatCleared, closeDestroyModal, closePopover]);
//
// {didMount && <KnowledgeBaseTour />}

return (
<>
<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
Expand Up @@ -16,8 +16,8 @@ import {
} 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 { KnowledgeBaseTour } from '.';
import { KNOWLEDGE_BASE_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';
Expand All @@ -38,7 +38,7 @@ jest.mock('@elastic/eui', () => {
const original = jest.requireActual('@elastic/eui');
return {
...original,
EuiTourStep: () => <div data-test-subj="attackDiscovery-tour-step-1" />,
EuiTourStep: () => <div data-test-subj="knowledgeBase-tour-step-1" />,
};
});
const mockedUseKibana = mockUseKibana();
Expand All @@ -49,8 +49,8 @@ const mockStore = createMockStore(undefined, undefined, undefined, storageMock);
const TestComponent = () => {
return (
<TestProviders store={mockStore}>
<div id={ATTACK_DISCOVERY_TOUR_CONFIG_ANCHORS.NAV_LINK} />
<AttackDiscoveryTour />
<div id={KNOWLEDGE_BASE_TOUR_CONFIG_ANCHORS.NAV_LINK} />
<KnowledgeBaseTour />
</TestProviders>
);
};
Expand Down Expand Up @@ -79,59 +79,59 @@ describe('Attack discovery tour', () => {
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();
expect(screen.queryByTestId('knowledgeBase-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, {
storageMock.set(NEW_FEATURES_TOUR_STORAGE_KEYS.KNOWLEDGE_BASE, {
currentTourStep: 1,
isTourActive: false,
});
render(<TestComponent />);
expect(screen.queryByTestId('attackDiscovery-tour-step-1')).toBeNull();
expect(screen.queryByTestId('attackDiscovery-tour-step-2')).toBeNull();
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 attack discovery', () => {
storageMock.set(NEW_FEATURES_TOUR_STORAGE_KEYS.ATTACK_DISCOVERY, {
it('should not render any tour steps when tour is on step 2 and page is not knowledge base', () => {
storageMock.set(NEW_FEATURES_TOUR_STORAGE_KEYS.KNOWLEDGE_BASE, {
currentTourStep: 2,
isTourActive: true,
});
const { debug } = render(<TestComponent />);
expect(screen.queryByTestId('attackDiscovery-tour-step-1')).toBeNull();
expect(screen.queryByTestId('knowledgeBase-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();
expect(getByTestId('knowledgeBase-tour-step-1')).toBeInTheDocument();
});

it('should render tour video when tour is on step 2 and page is attack discovery', () => {
it('should render tour video when tour is on step 2 and page is knowledge base', () => {
(useRouteSpy as jest.Mock).mockReturnValue([
{ ...mockRouteSpy, pageName: SecurityPageName.attackDiscovery },
{ ...mockRouteSpy, pageName: SecurityPageName.knowledgeBase },
]);
storageMock.set(NEW_FEATURES_TOUR_STORAGE_KEYS.ATTACK_DISCOVERY, {
storageMock.set(NEW_FEATURES_TOUR_STORAGE_KEYS.KNOWLEDGE_BASE, {
currentTourStep: 2,
isTourActive: true,
});
const { getByTestId } = render(<TestComponent />);
expect(screen.queryByTestId('attackDiscovery-tour-step-1')).toBeNull();
expect(getByTestId('attackDiscovery-tour-step-2')).toBeInTheDocument();
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 attack discovery', () => {
it('should advance to tour step 2 when page is knowledge base', () => {
(useRouteSpy as jest.Mock).mockReturnValue([
{ ...mockRouteSpy, pageName: SecurityPageName.attackDiscovery },
{ ...mockRouteSpy, pageName: SecurityPageName.knowledgeBase },
]);
storageMock.set(NEW_FEATURES_TOUR_STORAGE_KEYS.ATTACK_DISCOVERY, {
storageMock.set(NEW_FEATURES_TOUR_STORAGE_KEYS.KNOWLEDGE_BASE, {
currentTourStep: 1,
isTourActive: true,
});
render(<TestComponent />);
expect(
storageMock.get(NEW_FEATURES_TOUR_STORAGE_KEYS.ATTACK_DISCOVERY).currentTourStep
).toEqual(2);
expect(storageMock.get(NEW_FEATURES_TOUR_STORAGE_KEYS.KNOWLEDGE_BASE).currentTourStep).toEqual(
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,14 @@

import * as i18n from './translations';

export const ATTACK_DISCOVERY_TOUR_CONFIG_ANCHORS = {
export const KNOWLEDGE_BASE_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,
// anchor: KNOWLEDGE_BASE_TOUR_CONFIG_ANCHORS.NAV_LINK,
stephmilovic marked this conversation as resolved.
Show resolved Hide resolved
};

export const tourConfig = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* 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: 'Introducing knowledge base',
}
);

export const KNOWLEDGE_BASE_TOUR_KNOWLEDGE_BASE_DESC = i18n.translate(
'xpack.elasticAssistant.knowledgeBase.tour.navStep.desc',
{
defaultMessage: 'Leverage custom knowledge base entries.',
}
);

export const KNOWLEDGE_BASE_TOUR_VIDEO_STEP_TITLE = i18n.translate(
'xpack.elasticAssistant.knowledgeBase.tour.videoStep.title',
{
defaultMessage: 'Start knowledge basing',
}
);

export const KNOWLEDGE_BASE_TOUR_VIDEO_STEP_DESC = i18n.translate(
'xpack.elasticAssistant.knowledgeBase.tour.videoStep.desc',
{
defaultMessage:
'Dive into knowledge base entries with our intuitive AI technology, designed to elevate your productivity instantly.',
}
);

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',
}
);
Loading