[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:
Jatin Kathuria 2023-12-05 23:22:21 +01:00 committed by GitHub
parent 7d990cf749
commit 9b39d81c83
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 405 additions and 76 deletions

View file

@ -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

View file

@ -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) => {

View file

@ -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 =

View file

@ -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"

View file

@ -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>
);

View file

@ -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 pages 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 pages time range.',
}
);

View file

@ -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>
);
};

View file

@ -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"

View file

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

View file

@ -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();
});
});
});

View file

@ -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);

View file

@ -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,
};

View file

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