mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
# Backport This will backport the following commits from `main` to `8.12`: - [[Security Solution] Timeline UI refactor revision - Design feedback (#173015)](https://github.com/elastic/kibana/pull/173015) <!--- Backport version: 8.9.8 --> ### 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-13T23:17:19Z","message":"[Security Solution] Timeline UI refactor revision - Design feedback (#173015)\n\n## Summary\r\n\r\nThis PR implements Design feedback for the Timeline UI refactoring:\r\n\r\n1. Adds ⨁ back in timeline bottom bar.\r\n\r\n\r\n2. Add `Unsaved` badge for better visibility\r\n\r\n\r\n3. Adds a new tour step for `Add to Favorites` Icon.\r\n\r\n\r\n2d4f3e2e
-7868-4fbb-8a73-c9b427d86e04\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n### Checklist\r\n\r\nDelete any items that are not applicable to this PR.\r\n\r\n- [x] Any text added follows [EUI's writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing), uses\r\nsentence case text and includes [i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n- [x]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas added for features that require explanation or tutorials\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n- [x] [Flaky Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was\r\nused on any tests changed","sha":"1ba247ee719565ce3dd345ea0138a60bd71a6656","branchLabelMapping":{"^v8.13.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","backport missing","Team:Threat Hunting:Investigations","ci:cloud-deploy","v8.12.0","ci:project-persist-deployment","v8.13.0"],"number":173015,"url":"https://github.com/elastic/kibana/pull/173015","mergeCommit":{"message":"[Security Solution] Timeline UI refactor revision - Design feedback (#173015)\n\n## Summary\r\n\r\nThis PR implements Design feedback for the Timeline UI refactoring:\r\n\r\n1. Adds ⨁ back in timeline bottom bar.\r\n\r\n\r\n2. Add `Unsaved` badge for better visibility\r\n\r\n\r\n3. Adds a new tour step for `Add to Favorites` Icon.\r\n\r\n\r\n2d4f3e2e
-7868-4fbb-8a73-c9b427d86e04\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n### Checklist\r\n\r\nDelete any items that are not applicable to this PR.\r\n\r\n- [x] Any text added follows [EUI's writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing), uses\r\nsentence case text and includes [i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n- [x]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas added for features that require explanation or tutorials\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n- [x] [Flaky Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was\r\nused on any tests changed","sha":"1ba247ee719565ce3dd345ea0138a60bd71a6656"}},"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/173015","number":173015,"mergeCommit":{"message":"[Security Solution] Timeline UI refactor revision - Design feedback (#173015)\n\n## Summary\r\n\r\nThis PR implements Design feedback for the Timeline UI refactoring:\r\n\r\n1. Adds ⨁ back in timeline bottom bar.\r\n\r\n\r\n2. Add `Unsaved` badge for better visibility\r\n\r\n\r\n3. Adds a new tour step for `Add to Favorites` Icon.\r\n\r\n\r\n2d4f3e2e
-7868-4fbb-8a73-c9b427d86e04\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n### Checklist\r\n\r\nDelete any items that are not applicable to this PR.\r\n\r\n- [x] Any text added follows [EUI's writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing), uses\r\nsentence case text and includes [i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n- [x]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas added for features that require explanation or tutorials\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n- [x] [Flaky Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was\r\nused on any tests changed","sha":"1ba247ee719565ce3dd345ea0138a60bd71a6656"}}]}] BACKPORT-->
This commit is contained in:
parent
292b615297
commit
bb19b546b5
20 changed files with 222 additions and 88 deletions
|
@ -9,6 +9,7 @@ import dateMath from '@kbn/datemath';
|
|||
import type {
|
||||
EuiSuperDatePickerProps,
|
||||
EuiSuperDatePickerRecentRange,
|
||||
EuiSuperUpdateButtonProps,
|
||||
OnRefreshChangeProps,
|
||||
OnRefreshProps,
|
||||
OnTimeChangeProps,
|
||||
|
@ -42,6 +43,10 @@ import {
|
|||
} from './selectors';
|
||||
import type { Inputs } from '../../store/inputs/model';
|
||||
|
||||
const refreshButtonProps: EuiSuperUpdateButtonProps = {
|
||||
fill: false,
|
||||
};
|
||||
|
||||
const MAX_RECENTLY_USED_RANGES = 9;
|
||||
|
||||
interface Range {
|
||||
|
@ -219,6 +224,7 @@ export const SuperDatePickerComponent = React.memo<SuperDatePickerProps>(
|
|||
isDisabled={disabled}
|
||||
width={width}
|
||||
compressed={compressed}
|
||||
updateButtonProps={refreshButtonProps}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -62,6 +62,56 @@ exports[`Flyout rendering it renders correctly against snapshot 1`] = `
|
|||
<div
|
||||
class="euiFlexGroup emotion-euiFlexGroup-xs-flexStart-center-row"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem emotion-euiFlexItem-growZero"
|
||||
>
|
||||
<div
|
||||
class="euiFlexItem emotion-euiFlexItem-growZero"
|
||||
>
|
||||
<div
|
||||
class="euiPopover emotion-euiPopover"
|
||||
id="timelineSettingsPopover"
|
||||
>
|
||||
<div
|
||||
class="euiPopover__anchor css-16vtueo-render"
|
||||
>
|
||||
<button
|
||||
aria-label="Add new timeline or template"
|
||||
class="euiButtonIcon add-timeline-button emotion-euiButtonIcon-xs-empty-primary"
|
||||
data-test-subj="timeline-create-open-control"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="plusInCircle"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem emotion-euiFlexItem-growZero"
|
||||
>
|
||||
<button
|
||||
aria-label="Add to favorites"
|
||||
aria-pressed="false"
|
||||
class="euiButtonIcon emotion-euiButtonIcon-xs-empty-primary"
|
||||
data-test-subj="timeline-favorite-empty-star"
|
||||
id="add-to-favorites"
|
||||
title="Add to favorites"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="starEmpty"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem emotion-euiFlexItem-growZero"
|
||||
>
|
||||
|
@ -109,31 +159,21 @@ exports[`Flyout rendering it renders correctly against snapshot 1`] = `
|
|||
data-test-subj="timeline-status"
|
||||
>
|
||||
<span
|
||||
class="emotion-euiTextColor-warning"
|
||||
class="euiBadge emotion-euiBadge-warning"
|
||||
title="Unsaved"
|
||||
>
|
||||
Unsaved
|
||||
<span
|
||||
class="euiBadge__content emotion-euiBadge__content"
|
||||
>
|
||||
<span
|
||||
class="euiBadge__text emotion-euiBadge__text"
|
||||
>
|
||||
Unsaved
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="euiFlexItem emotion-euiFlexItem-growZero"
|
||||
>
|
||||
<button
|
||||
aria-label="Add to favorites"
|
||||
aria-pressed="false"
|
||||
class="euiButtonIcon emotion-euiButtonIcon-xs-empty-primary"
|
||||
data-test-subj="timeline-favorite-empty-star"
|
||||
title="Add to favorites"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="euiButtonIcon__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="starEmpty"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { useKibana } from '../../../../common/lib/kibana/kibana_react';
|
||||
import { APP_ID } from '../../../../../common';
|
||||
import type { TimelineTabs } from '../../../../../common/types';
|
||||
|
@ -25,6 +26,12 @@ interface TimelineActionMenuProps {
|
|||
activeTab: TimelineTabs;
|
||||
}
|
||||
|
||||
const VerticalDivider = styled.span`
|
||||
width: 0px;
|
||||
height: 20px;
|
||||
border-left: 1px solid ${({ theme }) => theme.eui.euiColorLightShade};
|
||||
`;
|
||||
|
||||
const TimelineActionMenuComponent = ({
|
||||
mode = 'normal',
|
||||
timelineId,
|
||||
|
@ -49,11 +56,6 @@ const TimelineActionMenuComponent = ({
|
|||
<EuiFlexItem data-test-subj="open-timeline-action">
|
||||
<OpenTimelineAction />
|
||||
</EuiFlexItem>
|
||||
{userCasesPermissions.create && userCasesPermissions.read ? (
|
||||
<EuiFlexItem>
|
||||
<AddToCaseButton timelineId={timelineId} />
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
<EuiFlexItem data-test-subj="inspect-timeline-action">
|
||||
<InspectButton
|
||||
compact={mode === 'compact'}
|
||||
|
@ -63,6 +65,16 @@ const TimelineActionMenuComponent = ({
|
|||
title=""
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{userCasesPermissions.create && userCasesPermissions.read ? (
|
||||
<>
|
||||
<EuiFlexItem>
|
||||
<VerticalDivider />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<AddToCaseButton timelineId={timelineId} />
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
) : null}
|
||||
<EuiFlexItem data-test-subj="save-timeline-action">
|
||||
<SaveTimelineButton timelineId={timelineId} />
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -52,11 +52,11 @@ jest.mock('../../../containers/all', () => {
|
|||
});
|
||||
|
||||
jest.mock('../../timeline/properties/new_template_timeline', () => ({
|
||||
NewTemplateTimeline: jest.fn(() => <div>{'Create new timeline template'}</div>),
|
||||
NewTemplateTimeline: jest.fn(() => <div>{'Create new Timeline template'}</div>),
|
||||
}));
|
||||
|
||||
jest.mock('../../timeline/properties/helpers', () => ({
|
||||
NewTimeline: jest.fn().mockReturnValue(<div>{'Create new timeline'}</div>),
|
||||
NewTimeline: jest.fn().mockReturnValue(<div>{'Create new Timeline'}</div>),
|
||||
}));
|
||||
|
||||
jest.mock('../../../../common/containers/source', () => ({
|
||||
|
|
|
@ -39,10 +39,10 @@ const AddTimelineButtonComponent: React.FC<AddTimelineButtonComponentProps> = ({
|
|||
() => (
|
||||
<EuiButtonIcon
|
||||
className={ADD_TIMELINE_BUTTON_CLASS_NAME}
|
||||
data-test-subj="settings-plus-in-circle"
|
||||
data-test-subj="timeline-create-open-control"
|
||||
iconType="plusInCircle"
|
||||
iconSize="m"
|
||||
color="primary"
|
||||
size="m"
|
||||
onClick={onButtonClick}
|
||||
aria-label={i18n.ADD_TIMELINE}
|
||||
/>
|
||||
|
|
|
@ -28,6 +28,7 @@ import { TimelineActionMenu } from '../action_menu';
|
|||
import { AddToFavoritesButton } from '../../timeline/properties/helpers';
|
||||
import { TimelineStatusInfo } from './timeline_status_info';
|
||||
import { timelineDefaults } from '../../../store/timeline/defaults';
|
||||
import { AddTimelineButton } from '../add_timeline_button';
|
||||
|
||||
interface FlyoutHeaderPanelProps {
|
||||
timelineId: string;
|
||||
|
@ -141,6 +142,14 @@ const FlyoutHeaderPanelComponent: React.FC<FlyoutHeaderPanelProps> = ({ timeline
|
|||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="xs" alignItems="center" responsive={false}>
|
||||
{!show ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<AddTimelineButton timelineId={timelineId} />
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
<EuiFlexItem grow={false}>
|
||||
<AddToFavoritesButton timelineId={timelineId} compact />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ActiveTimelinesContainer grow={false}>
|
||||
<ActiveTimelines
|
||||
|
@ -154,9 +163,6 @@ const FlyoutHeaderPanelComponent: React.FC<FlyoutHeaderPanelProps> = ({ timeline
|
|||
<EuiFlexItem grow={false}>
|
||||
<TimelineStatusInfo status={timelineStatus} updated={updated} changed={changed} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<AddToFavoritesButton timelineId={timelineId} compact />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
{show && (
|
||||
|
|
|
@ -28,18 +28,6 @@ describe('TestComponent', () => {
|
|||
|
||||
it('should render the status correctly when timeline has unsaved changes', () => {
|
||||
render(<TestComponent status={TimelineStatus.active} changed={true} updated={Date.now()} />);
|
||||
expect(screen.getByText('Has unsaved changes')).toBeVisible();
|
||||
});
|
||||
|
||||
it('should render the status correctly when timeline is saved', () => {
|
||||
const updatedTime = Date.now();
|
||||
render(<TestComponent status={TimelineStatus.active} updated={updatedTime} />);
|
||||
expect(screen.getByText('Saved')).toBeVisible();
|
||||
});
|
||||
|
||||
it('should render the status correctly when timeline is saved some time ago', () => {
|
||||
const updatedTime = Date.now() - 10000;
|
||||
render(<TestComponent status={TimelineStatus.active} updated={updatedTime} />);
|
||||
expect(screen.getByTestId('timeline-status')).toHaveTextContent(/Saved10 seconds ago/);
|
||||
expect(screen.getByText('Unsaved changes')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,8 +6,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiTextColor, EuiText } from '@elastic/eui';
|
||||
import { FormattedRelative } from '@kbn/i18n-react';
|
||||
import { EuiText, EuiBadge } from '@elastic/eui';
|
||||
|
||||
import styled from 'styled-components';
|
||||
import { TimelineStatus } from '../../../../../common/api/timeline';
|
||||
|
@ -29,21 +28,13 @@ export const TimelineStatusInfo = React.memo<TimelineStatusInfoProps>(
|
|||
|
||||
let statusContent: React.ReactNode = null;
|
||||
if (isUnsaved || !updated) {
|
||||
statusContent = <EuiTextColor color="warning">{i18n.UNSAVED}</EuiTextColor>;
|
||||
statusContent = <EuiBadge color="warning">{i18n.UNSAVED}</EuiBadge>;
|
||||
} else if (changed) {
|
||||
statusContent = <EuiTextColor color="warning">{i18n.UNSAVED_CHANGES}</EuiTextColor>;
|
||||
} else {
|
||||
statusContent = (
|
||||
<>
|
||||
{i18n.SAVED}
|
||||
<FormattedRelative
|
||||
data-test-subj="timeline-status"
|
||||
key="timeline-status-autosaved"
|
||||
value={new Date(updated)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
statusContent = <EuiBadge color="warning">{i18n.UNSAVED_CHANGES}</EuiBadge>;
|
||||
}
|
||||
|
||||
if (!statusContent) return null;
|
||||
|
||||
return (
|
||||
<NoWrapText size="xs" data-test-subj="timeline-status">
|
||||
{statusContent}
|
||||
|
|
|
@ -26,7 +26,7 @@ export const SAVED = i18n.translate('xpack.securitySolution.timeline.properties.
|
|||
export const UNSAVED_CHANGES = i18n.translate(
|
||||
'xpack.securitySolution.timeline.properties.hasChangesLabel',
|
||||
{
|
||||
defaultMessage: 'Has unsaved changes',
|
||||
defaultMessage: 'Unsaved changes',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ import { defaultHeaders } from './body/column_headers/default_headers';
|
|||
import type { CellValueElementProps } from './cell_rendering';
|
||||
import { SourcererScopeName } from '../../../common/store/sourcerer/model';
|
||||
import { FlyoutHeaderPanel } from '../flyout/header';
|
||||
import type { TimelineId, RowRenderer } from '../../../../common/types/timeline';
|
||||
import type { TimelineId, RowRenderer, TimelineTabs } from '../../../../common/types/timeline';
|
||||
import { TimelineType } from '../../../../common/api/timeline';
|
||||
import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/hooks/use_selector';
|
||||
import { activeTimeline } from '../../containers/active_timeline_context';
|
||||
|
@ -82,6 +82,7 @@ const StatefulTimelineComponent: React.FC<Props> = ({
|
|||
initialized,
|
||||
show: isOpen,
|
||||
isLoading,
|
||||
activeTab,
|
||||
} = useDeepEqualSelector((state) =>
|
||||
pick(
|
||||
[
|
||||
|
@ -95,6 +96,7 @@ const StatefulTimelineComponent: React.FC<Props> = ({
|
|||
'initialized',
|
||||
'show',
|
||||
'isLoading',
|
||||
'activeTab',
|
||||
],
|
||||
getTimeline(state, timelineId) ?? timelineDefaults
|
||||
)
|
||||
|
@ -195,6 +197,18 @@ const StatefulTimelineComponent: React.FC<Props> = ({
|
|||
|
||||
const showTimelineTour = isOpen && !isLoading && canEditTimeline;
|
||||
|
||||
const handleSwitchToTab = useCallback(
|
||||
(tab: TimelineTabs) => {
|
||||
dispatch(
|
||||
timelineActions.setActiveTabTimeline({
|
||||
id: timelineId,
|
||||
activeTab: tab,
|
||||
})
|
||||
);
|
||||
},
|
||||
[timelineId, dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<TimelineContext.Provider value={timelineContext}>
|
||||
<TimelineContainer
|
||||
|
@ -230,7 +244,9 @@ const StatefulTimelineComponent: React.FC<Props> = ({
|
|||
/>
|
||||
</div>
|
||||
</TimelineContainer>
|
||||
{showTimelineTour ? <TimelineTour /> : null}
|
||||
{showTimelineTour ? (
|
||||
<TimelineTour activeTab={activeTab} switchToTab={handleSwitchToTab} />
|
||||
) : null}
|
||||
</TimelineContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -18,6 +18,7 @@ import { useShallowEqualSelector } from '../../../../common/hooks/use_selector';
|
|||
import * as i18n from './translations';
|
||||
import { useCreateTimelineButton } from './use_create_timeline';
|
||||
import { timelineDefaults } from '../../../store/timeline/defaults';
|
||||
import { TIMELINE_TOUR_CONFIG_ANCHORS } from '../tour/step_config';
|
||||
|
||||
const NotesCountBadge = styled(EuiBadge)`
|
||||
margin-left: 5px;
|
||||
|
@ -56,7 +57,9 @@ const AddToFavoritesButtonComponent: React.FC<AddToFavoritesButtonProps> = ({
|
|||
|
||||
return compact ? (
|
||||
<EuiButtonIcon
|
||||
id={TIMELINE_TOUR_CONFIG_ANCHORS.ADD_TO_FAVORITES}
|
||||
iconType={isFavorite ? 'starFilled' : 'starEmpty'}
|
||||
iconSize="m"
|
||||
isSelected={isFavorite}
|
||||
onClick={handleClick}
|
||||
data-test-subj={`timeline-favorite-${isFavorite ? 'filled' : 'empty'}-star`}
|
||||
|
@ -66,6 +69,7 @@ const AddToFavoritesButtonComponent: React.FC<AddToFavoritesButtonProps> = ({
|
|||
/>
|
||||
) : (
|
||||
<EuiButton
|
||||
id={TIMELINE_TOUR_CONFIG_ANCHORS.ADD_TO_FAVORITES}
|
||||
isSelected={isFavorite}
|
||||
fill={isFavorite}
|
||||
iconType={isFavorite ? 'starFilled' : 'starEmpty'}
|
||||
|
|
|
@ -56,14 +56,14 @@ export const NOTES = i18n.translate('xpack.securitySolution.timeline.properties.
|
|||
export const NEW_TIMELINE = i18n.translate(
|
||||
'xpack.securitySolution.timeline.properties.newTimelineButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Create new timeline',
|
||||
defaultMessage: 'Create new Timeline',
|
||||
}
|
||||
);
|
||||
|
||||
export const NEW_TEMPLATE_TIMELINE = i18n.translate(
|
||||
'xpack.securitySolution.timeline.properties.newTemplateTimelineButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Create new timeline template',
|
||||
defaultMessage: 'Create new Timeline template',
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -11,15 +11,18 @@ 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 { TimelineTabs } from '../../../../../common/types';
|
||||
|
||||
jest.mock(
|
||||
'../../../../detection_engine/rule_management_ui/components/rules_table/rules_table/guided_onboarding/use_is_element_mounted'
|
||||
);
|
||||
|
||||
const switchTabMock = jest.fn();
|
||||
|
||||
const TestComponent = () => {
|
||||
return (
|
||||
<TestProviders>
|
||||
<TimelineTour />
|
||||
<TimelineTour activeTab={TimelineTabs.query} switchToTab={switchTabMock} />
|
||||
{Object.values(TIMELINE_TOUR_CONFIG_ANCHORS).map((anchor) => {
|
||||
return <div id={anchor} key={anchor} />;
|
||||
})}
|
||||
|
@ -58,6 +61,12 @@ describe('Timeline Tour', () => {
|
|||
|
||||
fireEvent.click(screen.getByText('Next'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('timeline-tour-step-4')).toBeVisible();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText('Next'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Finish tour')).toBeVisible();
|
||||
});
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
|
||||
import React, { useEffect, useCallback, useState } from 'react';
|
||||
import { EuiButton, EuiButtonEmpty, EuiTourStep } from '@elastic/eui';
|
||||
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';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
|
@ -25,7 +26,13 @@ interface TourState {
|
|||
tourSubtitle: string;
|
||||
}
|
||||
|
||||
const TimelineTourComp = () => {
|
||||
interface TimelineTourProps {
|
||||
activeTab: TimelineTabs;
|
||||
switchToTab: (tab: TimelineTabs) => void;
|
||||
}
|
||||
|
||||
const TimelineTourComp = (props: TimelineTourProps) => {
|
||||
const { activeTab, switchToTab } = props;
|
||||
const {
|
||||
services: { storage },
|
||||
} = useKibana();
|
||||
|
@ -86,6 +93,12 @@ const TimelineTourComp = () => {
|
|||
|
||||
const isElementAtCurrentStepMounted = useIsElementMounted(nextEl);
|
||||
|
||||
const currentStepConfig = timelineTourSteps[tourState.currentTourStep - 1];
|
||||
|
||||
if (currentStepConfig?.timelineTab && currentStepConfig.timelineTab !== activeTab) {
|
||||
switchToTab(currentStepConfig.timelineTab);
|
||||
}
|
||||
|
||||
if (!tourState.isTourActive || !isElementAtCurrentStepMounted) {
|
||||
return null;
|
||||
}
|
||||
|
@ -93,14 +106,16 @@ const TimelineTourComp = () => {
|
|||
return (
|
||||
<>
|
||||
{timelineTourSteps.map((steps, idx) => {
|
||||
if (tourState.currentTourStep !== idx + 1) return null;
|
||||
const stepCount = idx + 1;
|
||||
if (tourState.currentTourStep !== stepCount) return null;
|
||||
const panelProps = {
|
||||
'data-test-subj': `timeline-tour-step-${idx + 1}`,
|
||||
};
|
||||
return (
|
||||
<EuiTourStep
|
||||
panelProps={{
|
||||
'data-test-subj': `timeline-tour-step-${idx + 1}`,
|
||||
}}
|
||||
panelProps={panelProps}
|
||||
key={idx}
|
||||
step={steps.step}
|
||||
step={stepCount}
|
||||
isStepOpen={tourState.isTourActive && tourState.currentTourStep === idx + 1}
|
||||
minWidth={tourState.tourPopoverWidth}
|
||||
stepsTotal={timelineTourSteps.length}
|
||||
|
@ -109,7 +124,7 @@ const TimelineTourComp = () => {
|
|||
content={steps.content}
|
||||
anchor={`#${steps.anchor}`}
|
||||
subtitle={tourConfig.tourSubtitle}
|
||||
footerAction={getFooterAction(steps.step)}
|
||||
footerAction={getFooterAction(stepCount)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { EuiText, EuiCode } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { TimelineTabs } from '../../../../../common/types';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const TIMELINE_TOUR_CONFIG_ANCHORS = {
|
||||
|
@ -15,17 +16,17 @@ export const TIMELINE_TOUR_CONFIG_ANCHORS = {
|
|||
DATA_VIEW: 'timeline-data-view',
|
||||
DATA_PROVIDER: 'toggle-data-provider',
|
||||
SAVE_TIMELINE: 'save-timeline-action',
|
||||
ADD_TO_FAVORITES: 'add-to-favorites',
|
||||
};
|
||||
|
||||
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"
|
||||
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>,
|
||||
|
@ -36,13 +37,25 @@ export const timelineTourSteps = [
|
|||
anchor: TIMELINE_TOUR_CONFIG_ANCHORS.ACTION_MENU,
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
title: i18n.TIMELINE_TOUR_ADD_TO_FAVORITES_STEP_TITLE,
|
||||
content: (
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.timeline.tour.addToFavorites.description"
|
||||
defaultMessage="Click to favorite your Timeline so you can quickly find it later."
|
||||
/>
|
||||
</EuiText>
|
||||
),
|
||||
anchor: TIMELINE_TOUR_CONFIG_ANCHORS.ADD_TO_FAVORITES,
|
||||
},
|
||||
{
|
||||
timelineTab: TimelineTabs.query,
|
||||
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"
|
||||
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>,
|
||||
}}
|
||||
|
@ -52,19 +65,18 @@ export const timelineTourSteps = [
|
|||
anchor: TIMELINE_TOUR_CONFIG_ANCHORS.DATA_VIEW,
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
timelineTab: TimelineTabs.query,
|
||||
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"
|
||||
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>,
|
||||
|
|
|
@ -31,7 +31,7 @@ export const TIMELINE_TOUR_DATA_PROVIDER_VISIBILITY_TITLE = i18n.translate(
|
|||
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',
|
||||
defaultMessage: 'Click to expand or collapse the query builder.',
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -49,6 +49,13 @@ export const TIMELINE_TOUR_CHANGE_DATA_VIEW_TITLE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const TIMELINE_TOUR_ADD_TO_FAVORITES_STEP_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.timeline.tour.addToFavorites.title',
|
||||
{
|
||||
defaultMessage: 'A new and intuitive way to favorite your Timeline',
|
||||
}
|
||||
);
|
||||
|
||||
export const TIMELINE_TOUR_NEXT = i18n.translate('xpack.securitySolution.timeline.tour.next', {
|
||||
defaultMessage: 'Next',
|
||||
});
|
||||
|
|
|
@ -39,6 +39,7 @@ import {
|
|||
clickingOnCreateTemplateFromTimelineBtn,
|
||||
closeTimeline,
|
||||
createNewTimelineTemplate,
|
||||
createTimelineTemplateOptionsPopoverBottomBar,
|
||||
expandEventAction,
|
||||
markAsFavorite,
|
||||
openTimelineTemplateFromSettings,
|
||||
|
@ -113,4 +114,10 @@ describe('Timeline Templates', { tags: ['@ess', '@serverless'] }, () => {
|
|||
cy.get(TIMELINE_FLYOUT_WRAPPER).should('have.css', 'visibility', 'visible');
|
||||
cy.get(TIMELINE_QUERY).should('have.text', getTimeline().query);
|
||||
});
|
||||
|
||||
it('should create timeline template from bottombar', () => {
|
||||
visit(TIMELINES_URL);
|
||||
createTimelineTemplateOptionsPopoverBottomBar();
|
||||
cy.get(TIMELINE_TITLE).should('have.text', 'Untitled template');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -169,10 +169,7 @@ describe('Timelines', (): void => {
|
|||
addNameToTimelineAndSave('Test');
|
||||
|
||||
// Saved
|
||||
cy.get(TIMELINE_STATUS).should('be.visible');
|
||||
cy.get(TIMELINE_STATUS)
|
||||
.invoke('text')
|
||||
.should('match', /^Saved/);
|
||||
cy.get(TIMELINE_STATUS).should('not.exist');
|
||||
|
||||
executeTimelineKQL('agent.name : *');
|
||||
|
||||
|
@ -180,7 +177,7 @@ describe('Timelines', (): void => {
|
|||
cy.get(TIMELINE_STATUS).should('be.visible');
|
||||
cy.get(TIMELINE_STATUS)
|
||||
.invoke('text')
|
||||
.should('match', /^Has unsaved changes/);
|
||||
.should('match', /^Unsaved changes/);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -228,7 +228,7 @@ export const TIMELINE_PANEL = `[data-test-subj="timeline-flyout-header-panel"]`;
|
|||
|
||||
export const TIMELINE_QUERY = '[data-test-subj="timelineQueryInput"]';
|
||||
|
||||
export const TIMELINE_SETTINGS_ICON = '[data-test-subj="settings-plus-in-circle"]';
|
||||
export const TIMELINE_SETTINGS_ICON = '[data-test-subj="timeline-create-open-control"]';
|
||||
|
||||
export const TIMELINE_SEARCH_OR_FILTER = '[data-test-subj="timeline-select-search-or-filter"]';
|
||||
|
||||
|
|
|
@ -336,6 +336,30 @@ export const openCreateTimelineOptionsPopover = () => {
|
|||
cy.get(NEW_TIMELINE_ACTION).filter(':visible').should('be.visible').click();
|
||||
};
|
||||
|
||||
export const createTimelineOptionsPopoverBottomBar = () => {
|
||||
recurse(
|
||||
() => {
|
||||
cy.get(TIMELINE_SETTINGS_ICON).filter(':visible').should('be.visible').click();
|
||||
return cy.get(CREATE_NEW_TIMELINE).eq(0);
|
||||
},
|
||||
(sub) => sub.is(':visible')
|
||||
);
|
||||
|
||||
cy.get(CREATE_NEW_TIMELINE).eq(0).should('be.visible').click();
|
||||
};
|
||||
|
||||
export const createTimelineTemplateOptionsPopoverBottomBar = () => {
|
||||
recurse(
|
||||
() => {
|
||||
cy.get(TIMELINE_SETTINGS_ICON).filter(':visible').should('be.visible').click();
|
||||
return cy.get(CREATE_NEW_TIMELINE_TEMPLATE).eq(0);
|
||||
},
|
||||
(sub) => sub.is(':visible')
|
||||
);
|
||||
|
||||
cy.get(CREATE_NEW_TIMELINE_TEMPLATE).eq(0).should('be.visible').click();
|
||||
};
|
||||
|
||||
export const closeCreateTimelineOptionsPopover = () => {
|
||||
cy.get(TIMELINE_SETTINGS_ICON).filter(':visible').should('be.visible').type('{esc}');
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue