mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Security Solution] Add new timeline changes tour (#172030)
## Summary
This PR add the new timeline changes tour.Below is the demo.
@nastasha-solomon , could you please help check the copy and let me know
if its looks okay or it needs change.
Translation messages can be found in below files:
1.
`x-pack/plugins/security_solution/public/timelines/components/timeline/tour/step_config.tsx`
2.
`x-pack/plugins/security_solution/public/timelines/components/timeline/tour/translations.ts`
3ba1a984
-e0b5-41c1-8c6e-3d35f50f7c66
### Checklist
Delete any items that are not applicable to this PR.
- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
---------
Co-authored-by: Steph Milovic <stephanie.milovic@elastic.co>
Co-authored-by: Tiago Costa <tiago.costa@elastic.co>
This commit is contained in:
parent
7d990cf749
commit
9b39d81c83
13 changed files with 405 additions and 76 deletions
|
@ -39,6 +39,22 @@ export class CommonPageObject extends FtrService {
|
|||
return url.toString();
|
||||
}
|
||||
|
||||
private async disableTours() {
|
||||
const NEW_FEATURES_TOUR_STORAGE_KEYS = {
|
||||
RULE_MANAGEMENT_PAGE: 'securitySolution.rulesManagementPage.newFeaturesTour.v8.9',
|
||||
TIMELINE: 'securitySolution.timeline.newFeaturesTour.v8.12',
|
||||
};
|
||||
|
||||
const tourStorageKeys = Object.values(NEW_FEATURES_TOUR_STORAGE_KEYS);
|
||||
const tourConfig = {
|
||||
isTourActive: false,
|
||||
};
|
||||
|
||||
for (const key of tourStorageKeys) {
|
||||
await this.browser.setLocalStorageItem(key, JSON.stringify(tourConfig));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logins to Kibana as default user and navigates to provided app
|
||||
* @param appUrl Kibana URL
|
||||
|
@ -55,6 +71,8 @@ export class CommonPageObject extends FtrService {
|
|||
await this.browser.setLocalStorageItem('home:welcome:show', 'false');
|
||||
}
|
||||
|
||||
await this.disableTours();
|
||||
|
||||
let currentUrl = await this.browser.getCurrentUrl();
|
||||
this.log.debug(`currentUrl = ${currentUrl}\n appUrl = ${appUrl}`);
|
||||
await this.testSubjects.find('kibanaChrome', 6 * this.defaultFindTimeout); // 60 sec waiting
|
||||
|
|
|
@ -50,6 +50,7 @@ export const request = <T = unknown>({
|
|||
const NEW_FEATURES_TOUR_STORAGE_KEYS = {
|
||||
RULE_MANAGEMENT_PAGE: 'securitySolution.rulesManagementPage.newFeaturesTour.v8.9',
|
||||
TIMELINES: 'securitySolution.security.timelineFlyoutHeader.saveTimelineTour',
|
||||
TIMELINE: 'securitySolution.timeline.newFeaturesTour.v8.12',
|
||||
};
|
||||
|
||||
const disableNewFeaturesTours = (window: Window) => {
|
||||
|
|
|
@ -443,6 +443,7 @@ export const RULES_TABLE_MAX_PAGE_SIZE = 100;
|
|||
export const NEW_FEATURES_TOUR_STORAGE_KEYS = {
|
||||
RULE_MANAGEMENT_PAGE: 'securitySolution.rulesManagementPage.newFeaturesTour.v8.11',
|
||||
TIMELINES: 'securitySolution.security.timelineFlyoutHeader.saveTimelineTour',
|
||||
TIMELINE: 'securitySolution.timeline.newFeaturesTour.v8.12',
|
||||
};
|
||||
|
||||
export const RULE_DETAILS_EXECUTION_LOG_TABLE_SHOW_METRIC_COLUMNS_STORAGE_KEY =
|
||||
|
|
|
@ -16,6 +16,7 @@ import { AddToCaseButton } from '../add_to_case_button';
|
|||
import { NewTimelineAction } from './new_timeline';
|
||||
import { SaveTimelineButton } from './save_timeline_button';
|
||||
import { OpenTimelineAction } from './open_timeline';
|
||||
import { TIMELINE_TOUR_CONFIG_ANCHORS } from '../../timeline/tour/step_config';
|
||||
|
||||
interface TimelineActionMenuProps {
|
||||
mode?: 'compact' | 'normal';
|
||||
|
@ -35,6 +36,7 @@ const TimelineActionMenuComponent = ({
|
|||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
id={TIMELINE_TOUR_CONFIG_ANCHORS.ACTION_MENU}
|
||||
gutterSize="xs"
|
||||
justifyContent="flexEnd"
|
||||
alignItems="center"
|
||||
|
|
|
@ -6,25 +6,20 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { EuiButton, EuiToolTip, EuiTourStep, EuiCode, EuiText, EuiButtonEmpty } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiButton, EuiToolTip } from '@elastic/eui';
|
||||
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
|
||||
import { TimelineStatus } from '../../../../../common/api/timeline';
|
||||
import { useUserPrivileges } from '../../../../common/components/user_privileges';
|
||||
import { useIsElementMounted } from '../../../../detection_engine/rule_management_ui/components/rules_table/rules_table/guided_onboarding/use_is_element_mounted';
|
||||
import { useLocalStorage } from '../../../../common/components/local_storage';
|
||||
|
||||
import { SaveTimelineModal } from './save_timeline_modal';
|
||||
import * as timelineTranslations from './translations';
|
||||
import { getTimelineStatusByIdSelector } from '../header/selectors';
|
||||
import { TIMELINE_TOUR_CONFIG_ANCHORS } from '../../timeline/tour/step_config';
|
||||
|
||||
export interface SaveTimelineButtonProps {
|
||||
timelineId: string;
|
||||
}
|
||||
|
||||
const SAVE_BUTTON_ELEMENT_ID = 'SAVE_BUTTON_ELEMENT_ID';
|
||||
const LOCAL_STORAGE_KEY = 'security.timelineFlyoutHeader.saveTimelineTour';
|
||||
|
||||
export const SaveTimelineButton = React.memo<SaveTimelineButtonProps>(({ timelineId }) => {
|
||||
const [showEditTimelineOverlay, setShowEditTimelineOverlay] = useState<boolean>(false);
|
||||
|
||||
|
@ -44,39 +39,14 @@ export const SaveTimelineButton = React.memo<SaveTimelineButtonProps>(({ timelin
|
|||
const {
|
||||
kibanaSecuritySolutionsPrivileges: { crud: canEditTimelinePrivilege },
|
||||
} = useUserPrivileges();
|
||||
const getTimelineStatus = useMemo(() => getTimelineStatusByIdSelector(), []);
|
||||
const {
|
||||
status: timelineStatus,
|
||||
isSaving,
|
||||
isLoading,
|
||||
show: isVisible,
|
||||
} = useDeepEqualSelector((state) => getTimelineStatus(state, timelineId));
|
||||
|
||||
const isSaveButtonMounted = useIsElementMounted(SAVE_BUTTON_ELEMENT_ID);
|
||||
const [timelineTourStatus, setTimelineTourStatus] = useLocalStorage({
|
||||
defaultValue: { isTourActive: true },
|
||||
key: LOCAL_STORAGE_KEY,
|
||||
isInvalidDefault: (valueFromStorage) => {
|
||||
return !valueFromStorage;
|
||||
},
|
||||
});
|
||||
const getTimelineStatus = useMemo(() => getTimelineStatusByIdSelector(), []);
|
||||
|
||||
const { status: timelineStatus, isSaving } = useDeepEqualSelector((state) =>
|
||||
getTimelineStatus(state, timelineId)
|
||||
);
|
||||
|
||||
const canEditTimeline = canEditTimelinePrivilege && timelineStatus !== TimelineStatus.immutable;
|
||||
// Why are we checking for so many flags here?
|
||||
// The tour popup should only show when timeline is fully populated and all necessary
|
||||
// elements are visible on screen. If we would not check for all these flags, the tour
|
||||
// popup would show too early and in the wrong place in the DOM.
|
||||
// The last flag, checks if the tour has been dismissed before.
|
||||
const showTimelineSaveTour =
|
||||
canEditTimeline &&
|
||||
isVisible &&
|
||||
!isLoading &&
|
||||
isSaveButtonMounted &&
|
||||
timelineTourStatus?.isTourActive;
|
||||
|
||||
const markTimelineSaveTourAsSeen = useCallback(() => {
|
||||
setTimelineTourStatus({ isTourActive: false });
|
||||
}, [setTimelineTourStatus]);
|
||||
|
||||
const isUnsaved = timelineStatus === TimelineStatus.draft;
|
||||
const tooltipContent = canEditTimeline ? null : timelineTranslations.CALL_OUT_UNAUTHORIZED_MSG;
|
||||
|
@ -89,6 +59,7 @@ export const SaveTimelineButton = React.memo<SaveTimelineButtonProps>(({ timelin
|
|||
>
|
||||
<>
|
||||
<EuiButton
|
||||
id={TIMELINE_TOUR_CONFIG_ANCHORS.SAVE_TIMELINE}
|
||||
fill
|
||||
color="primary"
|
||||
onClick={openEditTimeline}
|
||||
|
@ -97,7 +68,6 @@ export const SaveTimelineButton = React.memo<SaveTimelineButtonProps>(({ timelin
|
|||
isLoading={isSaving}
|
||||
disabled={!canEditTimeline}
|
||||
data-test-subj="save-timeline-action-btn"
|
||||
id={SAVE_BUTTON_ELEMENT_ID}
|
||||
>
|
||||
{timelineTranslations.SAVE}
|
||||
</EuiButton>
|
||||
|
@ -109,37 +79,6 @@ export const SaveTimelineButton = React.memo<SaveTimelineButtonProps>(({ timelin
|
|||
showWarning={false}
|
||||
/>
|
||||
) : null}
|
||||
{showTimelineSaveTour && (
|
||||
<EuiTourStep
|
||||
anchor={`#${SAVE_BUTTON_ELEMENT_ID}`}
|
||||
content={
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.timeline.flyout.saveTour.description"
|
||||
defaultMessage="Click {saveButton} to manually save changes."
|
||||
values={{
|
||||
saveButton: <EuiCode>{timelineTranslations.SAVE}</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
}
|
||||
isStepOpen={true}
|
||||
minWidth={300}
|
||||
step={1}
|
||||
stepsTotal={1}
|
||||
onFinish={markTimelineSaveTourAsSeen}
|
||||
footerAction={
|
||||
<EuiButtonEmpty
|
||||
onClick={markTimelineSaveTourAsSeen}
|
||||
data-test-subj="timeline-save-tour-close-button"
|
||||
>
|
||||
{timelineTranslations.SAVE_TOUR_CLOSE}
|
||||
</EuiButtonEmpty>
|
||||
}
|
||||
title={timelineTranslations.SAVE_TOUR_TITLE}
|
||||
anchorPosition="downCenter"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</EuiToolTip>
|
||||
);
|
||||
|
|
|
@ -11,15 +11,14 @@ export const LOCK_SYNC_MAIN_DATE_PICKER_TOOL_TIP = i18n.translate(
|
|||
'xpack.securitySolution.timeline.properties.lockDatePickerTooltip',
|
||||
{
|
||||
defaultMessage:
|
||||
'Disable syncing of date/time range between the currently viewed page and your timeline',
|
||||
'Click to disable syncing of query time range with the current page’s time range',
|
||||
}
|
||||
);
|
||||
|
||||
export const UNLOCK_SYNC_MAIN_DATE_PICKER_TOOL_TIP = i18n.translate(
|
||||
'xpack.securitySolution.timeline.properties.unlockDatePickerTooltip',
|
||||
{
|
||||
defaultMessage:
|
||||
'Enable syncing of date/time range between the currently viewed page and your timeline',
|
||||
defaultMessage: 'Click to sync the query time range with the current page’s time range.',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import { useDispatch } from 'react-redux';
|
|||
import styled from 'styled-components';
|
||||
|
||||
import { isTab } from '@kbn/timelines-plugin/public';
|
||||
import { useUserPrivileges } from '../../../common/components/user_privileges';
|
||||
import { timelineActions, timelineSelectors } from '../../store/timeline';
|
||||
import { timelineDefaults } from '../../store/timeline/defaults';
|
||||
import { defaultHeaders } from './body/column_headers/default_headers';
|
||||
|
@ -30,6 +31,7 @@ import { useTimelineFullScreen } from '../../../common/containers/use_full_scree
|
|||
import { EXIT_FULL_SCREEN_CLASS_NAME } from '../../../common/components/exit_full_screen';
|
||||
import { useResolveConflict } from '../../../common/hooks/use_resolve_conflict';
|
||||
import { sourcererSelectors } from '../../../common/store';
|
||||
import { TimelineTour } from './tour';
|
||||
|
||||
const TimelineTemplateBadge = styled.div`
|
||||
background: ${({ theme }) => theme.eui.euiColorVis3_behindText};
|
||||
|
@ -78,6 +80,8 @@ const StatefulTimelineComponent: React.FC<Props> = ({
|
|||
description,
|
||||
sessionViewConfig,
|
||||
initialized,
|
||||
show: isOpen,
|
||||
isLoading,
|
||||
} = useDeepEqualSelector((state) =>
|
||||
pick(
|
||||
[
|
||||
|
@ -89,11 +93,17 @@ const StatefulTimelineComponent: React.FC<Props> = ({
|
|||
'description',
|
||||
'sessionViewConfig',
|
||||
'initialized',
|
||||
'show',
|
||||
'isLoading',
|
||||
],
|
||||
getTimeline(state, timelineId) ?? timelineDefaults
|
||||
)
|
||||
);
|
||||
|
||||
const {
|
||||
kibanaSecuritySolutionsPrivileges: { crud: canEditTimeline },
|
||||
} = useUserPrivileges();
|
||||
|
||||
const { timelineFullScreen } = useTimelineFullScreen();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -183,6 +193,8 @@ const StatefulTimelineComponent: React.FC<Props> = ({
|
|||
const timelineContext = useMemo(() => ({ timelineId }), [timelineId]);
|
||||
const resolveConflictComponent = useResolveConflict();
|
||||
|
||||
const showTimelineTour = isOpen && !isLoading && canEditTimeline;
|
||||
|
||||
return (
|
||||
<TimelineContext.Provider value={timelineContext}>
|
||||
<TimelineContainer
|
||||
|
@ -218,6 +230,7 @@ const StatefulTimelineComponent: React.FC<Props> = ({
|
|||
/>
|
||||
</div>
|
||||
</TimelineContainer>
|
||||
{showTimelineTour ? <TimelineTour /> : null}
|
||||
</TimelineContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -28,6 +28,7 @@ import {
|
|||
DATA_PROVIDER_HIDDEN_POPULATED,
|
||||
DATA_PROVIDER_VISIBLE,
|
||||
} from './translations';
|
||||
import { TIMELINE_TOUR_CONFIG_ANCHORS } from '../tour/step_config';
|
||||
|
||||
interface Props {
|
||||
dataProviders: DataProvider[];
|
||||
|
@ -113,7 +114,7 @@ export const SearchOrFilter = React.memo<Props>(
|
|||
alignItems="flexStart"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexItem grow={false} id={TIMELINE_TOUR_CONFIG_ANCHORS.DATA_VIEW}>
|
||||
<Sourcerer scope={SourcererScopeName.timeline} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem data-test-subj="timeline-search-or-filter-search-container" grow={1}>
|
||||
|
@ -145,6 +146,7 @@ export const SearchOrFilter = React.memo<Props>(
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip content={dataProviderIconTooltipContent}>
|
||||
<EuiButtonIcon
|
||||
id={TIMELINE_TOUR_CONFIG_ANCHORS.DATA_PROVIDER}
|
||||
color={buttonColor}
|
||||
isSelected={isDataProviderVisible}
|
||||
iconType="timeline"
|
||||
|
|
|
@ -67,21 +67,21 @@ export const SEARCH_KQL_SELECTED_TEXT = i18n.translate(
|
|||
export const DATA_PROVIDER_HIDDEN_POPULATED = i18n.translate(
|
||||
'xpack.securitySolution.timeline.searchOrFilter.dataProviderToggle.hiddenAndPopulated',
|
||||
{
|
||||
defaultMessage: 'Query Builder is hidden. Click here to see the existing Queries',
|
||||
defaultMessage: 'Click to access the query in the query builder',
|
||||
}
|
||||
);
|
||||
|
||||
export const DATA_PROVIDER_VISIBLE = i18n.translate(
|
||||
'xpack.securitySolution.timeline.searchOrFilter.dataProviderToggle.visible',
|
||||
{
|
||||
defaultMessage: 'Click here to hide Query builder',
|
||||
defaultMessage: 'Click to collapse the query builder',
|
||||
}
|
||||
);
|
||||
|
||||
export const DATA_PROVIDER_HIDDEN_EMPTY = i18n.translate(
|
||||
'xpack.securitySolution.timeline.searchOrFilter.dataProviderToggle.hiddenAndEmpty',
|
||||
{
|
||||
defaultMessage: 'Click here to show the empty Query builder',
|
||||
defaultMessage: 'Click to expand the empty query builder',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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 { 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';
|
||||
|
||||
jest.mock(
|
||||
'../../../../detection_engine/rule_management_ui/components/rules_table/rules_table/guided_onboarding/use_is_element_mounted'
|
||||
);
|
||||
|
||||
const TestComponent = () => {
|
||||
return (
|
||||
<TestProviders>
|
||||
<TimelineTour />
|
||||
{Object.values(TIMELINE_TOUR_CONFIG_ANCHORS).map((anchor) => {
|
||||
return <div id={anchor} key={anchor} />;
|
||||
})}
|
||||
</TestProviders>
|
||||
);
|
||||
};
|
||||
|
||||
describe('Timeline Tour', () => {
|
||||
beforeAll(() => {
|
||||
(useIsElementMounted as jest.Mock).mockReturnValue(true);
|
||||
});
|
||||
|
||||
it('should not render tour steps when element are not mounted', () => {
|
||||
(useIsElementMounted as jest.Mock).mockReturnValueOnce(false);
|
||||
render(<TestComponent />);
|
||||
expect(screen.queryByTestId('timeline-tour-step-1')).toBeNull();
|
||||
});
|
||||
|
||||
it('should render tour steps when element are mounted', async () => {
|
||||
render(<TestComponent />);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* This timeline tour only valid for 8.12 release is not needed for 8.13
|
||||
*
|
||||
* */
|
||||
|
||||
import React, { useEffect, useCallback, useState } from 'react';
|
||||
import { EuiButton, EuiButtonEmpty, EuiTourStep } from '@elastic/eui';
|
||||
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';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { timelineTourSteps, tourConfig } from './step_config';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface TourState {
|
||||
currentTourStep: number;
|
||||
isTourActive: boolean;
|
||||
tourPopoverWidth: number;
|
||||
tourSubtitle: string;
|
||||
}
|
||||
|
||||
const TimelineTourComp = () => {
|
||||
const {
|
||||
services: { storage },
|
||||
} = useKibana();
|
||||
|
||||
const [tourState, setTourState] = useState<TourState>(() => {
|
||||
const restoredTourState = storage.get(NEW_FEATURES_TOUR_STORAGE_KEYS.TIMELINE);
|
||||
|
||||
if (restoredTourState != null) {
|
||||
return restoredTourState;
|
||||
}
|
||||
return tourConfig;
|
||||
});
|
||||
|
||||
const finishTour = useCallback(() => {
|
||||
setTourState((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
isTourActive: false,
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const nextStep = useCallback(() => {
|
||||
setTourState((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
currentTourStep: prev.currentTourStep + 1,
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
storage.set(NEW_FEATURES_TOUR_STORAGE_KEYS.TIMELINE, tourState);
|
||||
}, [tourState, storage]);
|
||||
|
||||
const getFooterAction = useCallback(
|
||||
(step: number) => {
|
||||
// if it's the last step, we don't want to show the next button
|
||||
return step === timelineTourSteps.length ? (
|
||||
<EuiButton color="success" size="s" onClick={finishTour}>
|
||||
{i18n.TIMELINE_TOUR_FINISH}
|
||||
</EuiButton>
|
||||
) : (
|
||||
[
|
||||
<EuiButtonEmpty size="s" color="text" onClick={finishTour}>
|
||||
{i18n.TIMELINE_TOUR_EXIT}
|
||||
</EuiButtonEmpty>,
|
||||
<EuiButton color="success" size="s" onClick={nextStep}>
|
||||
{i18n.TIMELINE_TOUR_NEXT}
|
||||
</EuiButton>,
|
||||
]
|
||||
);
|
||||
},
|
||||
[finishTour, nextStep]
|
||||
);
|
||||
|
||||
const nextEl = timelineTourSteps[tourState.currentTourStep - 1]?.anchor;
|
||||
|
||||
const isElementAtCurrentStepMounted = useIsElementMounted(nextEl);
|
||||
|
||||
if (!tourState.isTourActive || !isElementAtCurrentStepMounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{timelineTourSteps.map((steps, idx) => {
|
||||
if (tourState.currentTourStep !== idx + 1) return null;
|
||||
return (
|
||||
<EuiTourStep
|
||||
panelProps={{
|
||||
'data-test-subj': `timeline-tour-step-${idx + 1}`,
|
||||
}}
|
||||
key={idx}
|
||||
step={steps.step}
|
||||
isStepOpen={tourState.isTourActive && tourState.currentTourStep === idx + 1}
|
||||
minWidth={tourState.tourPopoverWidth}
|
||||
stepsTotal={timelineTourSteps.length}
|
||||
onFinish={finishTour}
|
||||
title={steps.title}
|
||||
content={steps.content}
|
||||
anchor={`#${steps.anchor}`}
|
||||
subtitle={tourConfig.tourSubtitle}
|
||||
footerAction={getFooterAction(steps.step)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const TimelineTour = React.memo(TimelineTourComp);
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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 { EuiText, EuiCode } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const TIMELINE_TOUR_CONFIG_ANCHORS = {
|
||||
ACTION_MENU: 'timeline-action-menu',
|
||||
DATA_VIEW: 'timeline-data-view',
|
||||
DATA_PROVIDER: 'toggle-data-provider',
|
||||
SAVE_TIMELINE: 'save-timeline-action',
|
||||
};
|
||||
|
||||
export const timelineTourSteps = [
|
||||
{
|
||||
step: 1,
|
||||
title: i18n.TIMELINE_TOUR_TIMELINE_ACTIONS_STEP_TITLE,
|
||||
content: (
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.timeline.tour.newTimeline.description"
|
||||
defaultMessage="Click {newButton} to create a new timeline. Click {openButton} to open an existing one"
|
||||
values={{
|
||||
newButton: <EuiCode>{i18n.TIMELINE_TOUR_NEW}</EuiCode>,
|
||||
openButton: <EuiCode>{i18n.TIMELINE_TOUR_OPEN}</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
),
|
||||
anchor: TIMELINE_TOUR_CONFIG_ANCHORS.ACTION_MENU,
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
title: i18n.TIMELINE_TOUR_CHANGE_DATA_VIEW_TITLE,
|
||||
content: (
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.timeline.tour.changeDataView.description"
|
||||
defaultMessage="Click the {dataViewButton} menu to choose the event or alert data that you want to display"
|
||||
values={{
|
||||
dataViewButton: <EuiCode> {i18n.TIMELINE_TOUR_DATA_VIEW}</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
),
|
||||
anchor: TIMELINE_TOUR_CONFIG_ANCHORS.DATA_VIEW,
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
title: i18n.TIMELINE_TOUR_DATA_PROVIDER_VISIBILITY_TITLE,
|
||||
content: <EuiText>{i18n.TIMELINE_TOUR_DATA_PROVIDER_VISIBILITY_DESCRIPTION}</EuiText>,
|
||||
anchor: TIMELINE_TOUR_CONFIG_ANCHORS.DATA_PROVIDER,
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
title: i18n.TIMELINE_TOUR_SAVE_TIMELINE_STEP_TITLE,
|
||||
content: (
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.timeline.tour.saveTimeline.description"
|
||||
defaultMessage="Click {saveButton} to manually save new changes. While saving your Timeline, you can {editButton} its name and description or save it as a new Timeline"
|
||||
values={{
|
||||
saveButton: <EuiCode>{i18n.TIMELINE_TOUR_SAVE}</EuiCode>,
|
||||
editButton: <EuiCode>{i18n.TIMELINE_TOUR_EDIT}</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
),
|
||||
anchor: TIMELINE_TOUR_CONFIG_ANCHORS.SAVE_TIMELINE,
|
||||
},
|
||||
];
|
||||
|
||||
export const tourConfig = {
|
||||
currentTourStep: 1,
|
||||
isTourActive: true,
|
||||
tourPopoverWidth: 300,
|
||||
tourSubtitle: i18n.TIMELINE_TOUR_SUBTITLE,
|
||||
};
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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 TIMELINE_TOUR_SUBTITLE = i18n.translate(
|
||||
'xpack.securitySolution.timeline.tour.subTitle',
|
||||
{
|
||||
defaultMessage: 'Recent Timeline improvements',
|
||||
}
|
||||
);
|
||||
|
||||
export const TIMELINE_TOUR_TIMELINE_ACTIONS_STEP_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.timeline.tour.newTimeline.title',
|
||||
{
|
||||
defaultMessage: 'Actions are now easier to find',
|
||||
}
|
||||
);
|
||||
|
||||
export const TIMELINE_TOUR_DATA_PROVIDER_VISIBILITY_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.timeline.tour.dataProviderToggle.title',
|
||||
{
|
||||
defaultMessage: 'The query builder is collapsed by default',
|
||||
}
|
||||
);
|
||||
|
||||
export const TIMELINE_TOUR_DATA_PROVIDER_VISIBILITY_DESCRIPTION = i18n.translate(
|
||||
'xpack.securitySolution.timeline.tour.dataProviderToggle.description',
|
||||
{
|
||||
defaultMessage: 'Click to expand or collapse the query builder',
|
||||
}
|
||||
);
|
||||
|
||||
export const TIMELINE_TOUR_SAVE_TIMELINE_STEP_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.timeline.tour.saveTimeline.title',
|
||||
{
|
||||
defaultMessage: 'An easier way to save new changes',
|
||||
}
|
||||
);
|
||||
|
||||
export const TIMELINE_TOUR_CHANGE_DATA_VIEW_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.timeline.tour.changeDataView.title',
|
||||
{
|
||||
defaultMessage: 'The Data view menu has moved',
|
||||
}
|
||||
);
|
||||
|
||||
export const TIMELINE_TOUR_NEXT = i18n.translate('xpack.securitySolution.timeline.tour.next', {
|
||||
defaultMessage: 'Next',
|
||||
});
|
||||
|
||||
export const TIMELINE_TOUR_FINISH = i18n.translate('xpack.securitySolution.timeline.tour.finish', {
|
||||
defaultMessage: 'Finish tour',
|
||||
});
|
||||
|
||||
export const TIMELINE_TOUR_EXIT = i18n.translate('xpack.securitySolution.timeline.tour.exit', {
|
||||
defaultMessage: 'Exit tour',
|
||||
});
|
||||
|
||||
export const TIMELINE_TOUR_SAVE = i18n.translate('xpack.securitySolution.timeline.tour.save', {
|
||||
defaultMessage: 'Save',
|
||||
});
|
||||
|
||||
export const TIMELINE_TOUR_NEW = i18n.translate('xpack.securitySolution.timeline.tour.new', {
|
||||
defaultMessage: 'New',
|
||||
});
|
||||
|
||||
export const TIMELINE_TOUR_OPEN = i18n.translate('xpack.securitySolution.timeline.tour.open', {
|
||||
defaultMessage: 'Open',
|
||||
});
|
||||
|
||||
export const TIMELINE_TOUR_EDIT = i18n.translate('xpack.securitySolution.timeline.tour.edit', {
|
||||
defaultMessage: 'Edit',
|
||||
});
|
||||
|
||||
export const TIMELINE_TOUR_DATA_VIEW = i18n.translate(
|
||||
'xpack.securitySolution.timeline.tour.dataView',
|
||||
{
|
||||
defaultMessage: 'Data view',
|
||||
}
|
||||
);
|
Loading…
Add table
Add a link
Reference in a new issue