mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
# Backport This will backport the following commits from `main` to `8.12`: - [[Security solution] Timeline tour compatiable with timeline template (#173526)](https://github.com/elastic/kibana/pull/173526) <!--- Backport version: 8.9.7 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Jatin Kathuria","email":"jatin.kathuria@elastic.co"},"sourceCommit":{"committedDate":"2023-12-18T22:01:16Z","message":"[Security solution] Timeline tour compatiable with timeline template (#173526)\n\n## Summary\r\n\r\nHandles https://github.com/elastic/kibana/issues/173355.\r\n\r\nTimeline v8.12 changes tour had some steps in error, if user changed\r\nfrom `default` timeline to `template` timeline.\r\n\r\nThis PR makes step compatible both kinds of timeline. We have the\r\nability to choose which step shows up on which kind of timeline.\r\n\r\n\r\n\r\n80b96027
-5e38-4e17-914d-d6df77a070f3\r\n\r\n---------\r\n\r\nCo-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>","sha":"13f2779e5a4de368a81457e8684e2292049d6ce2","branchLabelMapping":{"^v8.13.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","Team:Threat Hunting:Investigations","v8.12.0","v8.13.0"],"number":173526,"url":"https://github.com/elastic/kibana/pull/173526","mergeCommit":{"message":"[Security solution] Timeline tour compatiable with timeline template (#173526)\n\n## Summary\r\n\r\nHandles https://github.com/elastic/kibana/issues/173355.\r\n\r\nTimeline v8.12 changes tour had some steps in error, if user changed\r\nfrom `default` timeline to `template` timeline.\r\n\r\nThis PR makes step compatible both kinds of timeline. We have the\r\nability to choose which step shows up on which kind of timeline.\r\n\r\n\r\n\r\n80b96027
-5e38-4e17-914d-d6df77a070f3\r\n\r\n---------\r\n\r\nCo-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>","sha":"13f2779e5a4de368a81457e8684e2292049d6ce2"}},"sourceBranch":"main","suggestedTargetBranches":["8.12"],"targetPullRequestStates":[{"branch":"8.12","label":"v8.12.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.13.0","labelRegex":"^v8.13.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/173526","number":173526,"mergeCommit":{"message":"[Security solution] Timeline tour compatiable with timeline template (#173526)\n\n## Summary\r\n\r\nHandles https://github.com/elastic/kibana/issues/173355.\r\n\r\nTimeline v8.12 changes tour had some steps in error, if user changed\r\nfrom `default` timeline to `template` timeline.\r\n\r\nThis PR makes step compatible both kinds of timeline. We have the\r\nability to choose which step shows up on which kind of timeline.\r\n\r\n\r\n\r\n80b96027
-5e38-4e17-914d-d6df77a070f3\r\n\r\n---------\r\n\r\nCo-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>","sha":"13f2779e5a4de368a81457e8684e2292049d6ce2"}}]}] BACKPORT--> Co-authored-by: Jatin Kathuria <jatin.kathuria@elastic.co>
This commit is contained in:
parent
96eedabdc4
commit
6ec37d289c
4 changed files with 86 additions and 15 deletions
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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} />;
|
||||
})}
|
||||
|
@ -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 />);
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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 = {
|
||||
|
@ -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}
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
@ -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>,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue