Skip to content

Commit

Permalink
[Security Solution] Repurpose attack discover tour into knowledge bas…
Browse files Browse the repository at this point in the history
…e tour (elastic#196615)
  • Loading branch information
stephmilovic authored Oct 23, 2024
1 parent 67de924 commit fa9bb19
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 fa9bb19

Please sign in to comment.