Skip to content

Commit

Permalink
[Security solution] Timeline tour compatiable with timeline template (e…
Browse files Browse the repository at this point in the history
…lastic#173526)

## Summary

Handles elastic#173355.

Timeline v8.12 changes tour had some steps in error, if user changed
from `default` timeline to `template` timeline.

This PR makes step compatible both kinds of timeline. We have the
ability to choose which step shows up on which kind of timeline.



https://github.com/elastic/kibana/assets/7485038/80b96027-5e38-4e17-914d-d6df77a070f3

---------

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
logeekal and kibanamachine authored Dec 18, 2023
1 parent 1c99f40 commit 13f2779
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,11 @@ const StatefulTimelineComponent: React.FC<Props> = ({
</div>
</TimelineContainer>
{showTimelineTour ? (
<TimelineTour activeTab={activeTab} switchToTab={handleSwitchToTab} />
<TimelineTour
activeTab={activeTab}
switchToTab={handleSwitchToTab}
timelineType={timelineType}
/>
) : null}
</TimelineContext.Provider>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,44 @@
*/

import React from 'react';
import type { TimelineTourProps } from '.';
import { TimelineTour } from '.';
import { TIMELINE_TOUR_CONFIG_ANCHORS } from './step_config';
import { useIsElementMounted } from '../../../../detection_engine/rule_management_ui/components/rules_table/rules_table/guided_onboarding/use_is_element_mounted';
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import { TestProviders } from '../../../../common/mock';
import {
createSecuritySolutionStorageMock,
kibanaObservable,
mockGlobalState,
SUB_PLUGINS_REDUCER,
TestProviders,
} from '../../../../common/mock';
import { TimelineTabs } from '../../../../../common/types';
import { TimelineType } from '../../../../../common/api/timeline';
import { createStore } from '../../../../common/store';
import { useKibana as mockUseKibana } from '../../../../common/lib/kibana/__mocks__';
import { useKibana } from '../../../../common/lib/kibana';

jest.mock(
'../../../../detection_engine/rule_management_ui/components/rules_table/rules_table/guided_onboarding/use_is_element_mounted'
);
jest.mock('../../../../common/lib/kibana');

const mockedUseKibana = mockUseKibana();

const switchTabMock = jest.fn();
const { storage: storageMock } = createSecuritySolutionStorageMock();
const mockStore = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storageMock);

const TestComponent = () => {
const TestComponent = (props: Partial<TimelineTourProps> = {}) => {
return (
<TestProviders>
<TimelineTour activeTab={TimelineTabs.query} switchToTab={switchTabMock} />
<TestProviders store={mockStore}>
<TimelineTour
activeTab={TimelineTabs.query}
switchToTab={switchTabMock}
timelineType={TimelineType.default}
{...props}
/>
{Object.values(TIMELINE_TOUR_CONFIG_ANCHORS).map((anchor) => {
return <div id={anchor} key={anchor} />;
})}
Expand All @@ -35,6 +56,18 @@ describe('Timeline Tour', () => {
(useIsElementMounted as jest.Mock).mockReturnValue(true);
});

beforeEach(() => {
(useKibana as jest.Mock).mockReturnValue({
...mockedUseKibana,
services: {
...mockedUseKibana.services,
storage: storageMock,
},
});

storageMock.clear();
});

it('should not render tour steps when element are not mounted', () => {
(useIsElementMounted as jest.Mock).mockReturnValueOnce(false);
render(<TestComponent />);
Expand Down Expand Up @@ -71,4 +104,29 @@ describe('Timeline Tour', () => {
expect(screen.queryByText('Finish tour')).toBeVisible();
});
});

it('should render different tour steps when timeline type is template', async () => {
render(<TestComponent timelineType={TimelineType.template} />);

await waitFor(() => {
expect(screen.getByTestId('timeline-tour-step-1')).toBeVisible();
});

fireEvent.click(screen.getByText('Next'));
await waitFor(() => {
expect(screen.getByTestId('timeline-tour-step-2')).toBeVisible();
});

fireEvent.click(screen.getByText('Next'));

await waitFor(() => {
expect(screen.getByTestId('timeline-tour-step-3')).toBeVisible();
});

fireEvent.click(screen.getByText('Next'));

await waitFor(() => {
expect(screen.queryByText('Finish tour')).toBeVisible();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
*
* */

import React, { useEffect, useCallback, useState } from 'react';
import React, { useEffect, useCallback, useState, useMemo } from 'react';
import { EuiButton, EuiButtonEmpty, EuiTourStep } from '@elastic/eui';
import type { TimelineType } from '../../../../../common/api/timeline';
import type { TimelineTabs } from '../../../../../common/types';
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 } from '../../../../../common/constants';
Expand All @@ -26,20 +27,26 @@ interface TourState {
tourSubtitle: string;
}

interface TimelineTourProps {
export interface TimelineTourProps {
activeTab: TimelineTabs;
timelineType: TimelineType;
switchToTab: (tab: TimelineTabs) => void;
}

const TimelineTourComp = (props: TimelineTourProps) => {
const { activeTab, switchToTab } = props;
const { activeTab, switchToTab, timelineType } = props;
const {
services: { storage },
} = useKibana();

const updatedTourSteps = useMemo(
() =>
timelineTourSteps.filter((step) => !step.timelineType || step.timelineType === timelineType),
[timelineType]
);

const [tourState, setTourState] = useState<TourState>(() => {
const restoredTourState = storage.get(NEW_FEATURES_TOUR_STORAGE_KEYS.TIMELINE);

if (restoredTourState != null) {
return restoredTourState;
}
Expand Down Expand Up @@ -71,7 +78,7 @@ const TimelineTourComp = (props: TimelineTourProps) => {
const getFooterAction = useCallback(
(step: number) => {
// if it's the last step, we don't want to show the next button
return step === timelineTourSteps.length ? (
return step === updatedTourSteps.length ? (
<EuiButton color="success" size="s" onClick={finishTour}>
{i18n.TIMELINE_TOUR_FINISH}
</EuiButton>
Expand All @@ -86,14 +93,14 @@ const TimelineTourComp = (props: TimelineTourProps) => {
]
);
},
[finishTour, nextStep]
[finishTour, nextStep, updatedTourSteps.length]
);

const nextEl = timelineTourSteps[tourState.currentTourStep - 1]?.anchor;
const nextEl = updatedTourSteps[tourState.currentTourStep - 1]?.anchor;

const isElementAtCurrentStepMounted = useIsElementMounted(nextEl);

const currentStepConfig = timelineTourSteps[tourState.currentTourStep - 1];
const currentStepConfig = updatedTourSteps[tourState.currentTourStep - 1];

if (currentStepConfig?.timelineTab && currentStepConfig.timelineTab !== activeTab) {
switchToTab(currentStepConfig.timelineTab);
Expand All @@ -105,7 +112,7 @@ const TimelineTourComp = (props: TimelineTourProps) => {

return (
<>
{timelineTourSteps.map((steps, idx) => {
{updatedTourSteps.map((steps, idx) => {
const stepCount = idx + 1;
if (tourState.currentTourStep !== stepCount) return null;
const panelProps = {
Expand All @@ -118,7 +125,7 @@ const TimelineTourComp = (props: TimelineTourProps) => {
step={stepCount}
isStepOpen={tourState.isTourActive && tourState.currentTourStep === idx + 1}
minWidth={tourState.tourPopoverWidth}
stepsTotal={timelineTourSteps.length}
stepsTotal={updatedTourSteps.length}
onFinish={finishTour}
title={steps.title}
content={steps.content}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { EuiText, EuiCode } from '@elastic/eui';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { TimelineType } from '../../../../../common/api/timeline';
import { TimelineTabs } from '../../../../../common/types';
import * as i18n from './translations';

Expand Down Expand Up @@ -65,6 +66,7 @@ export const timelineTourSteps = [
anchor: TIMELINE_TOUR_CONFIG_ANCHORS.DATA_VIEW,
},
{
timelineType: TimelineType.default,
timelineTab: TimelineTabs.query,
title: i18n.TIMELINE_TOUR_DATA_PROVIDER_VISIBILITY_TITLE,
content: <EuiText>{i18n.TIMELINE_TOUR_DATA_PROVIDER_VISIBILITY_DESCRIPTION}</EuiText>,
Expand Down

0 comments on commit 13f2779

Please sign in to comment.