[Security Solution] - use expandable flyout in timeline (#177087)

## Summary

This PR enables Timeline to have its own expandable flyout, independent
from the Security Solution app level flyout. This previous
[PR](https://github.com/elastic/kibana/pull/176457) added support to the
`kbn-expandable-flyout` package to be able to show multiple flyout at
the same time.

This PR focuses on:
- changing the location where we use the `<ExpandableFlyoutProvider>` to
wrap only the Security Solution pages. instead of the entire Security
Solution application
- wrapping `Timeline` with its own `<ExpandableFlyoutProvider>`
- opening the new expandable flyout from timeline for alerts, users and
host!!

### Notes

- the new expandable flyout opened from `Timeline` is saved in the url
under a new `timelineFlyout` parameter. The other Security Solution
flyout stays saved under `eventFlyout` so both can be displayed at the
same time
- introduced a new feature flag called
`expandableTimelineFlyoutEnabled`, enabled to `false` by default. The
code uses this flag in combination with the already existing
`securitySolution:enableExpandableFlyout` advanced settings to toggle
on/off the expandable flyout in `Timeline`

I had to make a small change to the timeline `z-index` value, from its
hardcoded value of `1000` to now `1001`. At the same time I'm setting
the `z-index` of the `eventFlyout` to `1000` and the `z-index` of the
`timelineFlyout` to `1002`. Finally, I changed the `z-index` of all the
other flyouts opened from the `Take action` button in the footer (like
_Add to new case_, _Add rule exception_...) to `1003`. This was the only
way to get the order correct after page refresh, and also work in
Serverless which has a different top level layout (for the top bar and
the left navigation panel).

### Testing

Make sure you have the flag turned on in your `kibana.yml` file:
`xpack.securitySolution.enableExperimental:
['expandableTimelineFlyoutEnabled']`

While the code changes are pretty minimal, emphasize should be done on
testing:
- verify that the non-timeline expandable flyout behavior hasn't changed
(in the Explore, Rule Preview, Alerts pages...)
- verify the advanced settings correctly toggles on and off the
expandable flyout for both the Security Solution app and Timeline
- verify the expandable flyout correctly shows up in Timeline on all the
pages where Timeline is present
- verify both flyout can be shown at the same time
- verify refreshing the page reloads correctly all flyouts open
- verify copying the url or using the Share Alert button at the top can
be correctly reopened in another tab

### Checklist

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

### TODO

- [x] fix unit tests
- [ ] fix e2e tests
- [x] figure out how to fix the UI in serverless (the flyout is
currently behind the left navigation)

Before change:


3b89f87f-d1fa-4635-8ff7-855795eb3796

After change:


6b341f11-054c-4885-b496-006f37b8d572

Serverless


df414140-31df-4941-869e-c177cfedd805

https://github.com/elastic/security-team/issues/7464

Details of file changed in [the comment
below](https://github.com/elastic/kibana/pull/177087#issuecomment-1988642537)
This commit is contained in:
Philippe Oberti 2024-03-25 10:47:23 +01:00 committed by GitHub
parent 2aa5d968e8
commit f8a6324444
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 614 additions and 288 deletions

View file

@ -7,6 +7,7 @@
*/
import React, { useMemo } from 'react';
import type { Interpolation, Theme } from '@emotion/react';
import { EuiFlyoutProps } from '@elastic/eui';
import { EuiFlexGroup, EuiFlyout } from '@elastic/eui';
import { useSectionSizes } from './hooks/use_sections_sizes';
@ -26,6 +27,10 @@ export interface ExpandableFlyoutProps extends Omit<EuiFlyoutProps, 'onClose'> {
* List of all registered panels available for render
*/
registeredPanels: Panel[];
/**
* Allows for custom styles to be passed to the EuiFlyout component
*/
customStyles?: Interpolation<Theme>;
}
/**
@ -36,6 +41,7 @@ export interface ExpandableFlyoutProps extends Omit<EuiFlyoutProps, 'onClose'> {
* is already rendered.
*/
export const ExpandableFlyout: React.FC<ExpandableFlyoutProps> = ({
customStyles,
registeredPanels,
...flyoutProps
}) => {
@ -83,7 +89,13 @@ export const ExpandableFlyout: React.FC<ExpandableFlyoutProps> = ({
}
return (
<EuiFlyout {...flyoutProps} size={flyoutWidth} ownFocus={false} onClose={closeFlyout}>
<EuiFlyout
{...flyoutProps}
size={flyoutWidth}
ownFocus={false}
onClose={closeFlyout}
css={customStyles}
>
<EuiFlexGroup
direction={leftSection ? 'row' : 'column'}
wrap={false}

View file

@ -226,6 +226,8 @@ export const getFieldEditorOpener =
},
maskProps: {
className: 'indexPatternFieldEditorMaskOverlay',
// // EUI TODO: This z-index override of EuiOverlayMask is a workaround, and ideally should be resolved with a cleaner UI/UX flow long-term
style: 'z-index: 1003', // we need this flyout to be above the timeline flyout (which has a z-index of 1002)
},
}
);

View file

@ -40,12 +40,11 @@ export const CreateCaseFlyout = React.memo<CreateCaseFlyoutProps>(
<>
<ReactQueryDevtools initialIsOpen={false} />
<EuiFlyout
css={css`
z-index: ${euiTheme.levels.flyout};
`}
onClose={handleCancel}
tour-step="create-case-flyout"
data-test-subj="create-case-flyout"
// EUI TODO: This z-index override of EuiOverlayMask is a workaround, and ideally should be resolved with a cleaner UI/UX flow long-term
maskProps={{ style: `z-index: ${(euiTheme.levels.flyout as number) + 3}` }} // we need this flyout to be above the timeline flyout (which has a z-index of 1002)
>
<EuiFlyoutHeader data-test-subj="create-case-flyout-header" hasBorder>
<EuiTitle size="m">

View file

@ -111,6 +111,12 @@ export const allowedExperimentalValues = Object.freeze({
*/
expandableEventFlyoutEnabled: false,
/**
* Enables expandable flyout in timeline
*/
expandableTimelineFlyoutEnabled: false,
/*
/**
* Enables new Set of filters on the Alerts page.
*/

View file

@ -12,8 +12,8 @@ import { IS_DRAGGING_CLASS_NAME } from '@kbn/securitysolution-t-grid';
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
import type { KibanaPageTemplateProps } from '@kbn/shared-ux-page-kibana-template';
import { ExpandableFlyoutProvider } from '@kbn/expandable-flyout';
import { EXPANDABLE_FLYOUT_URL_KEY } from '../../../common/hooks/use_url_state';
import { SecuritySolutionFlyout } from '../../../flyout';
import { URL_PARAM_KEY } from '../../../common/hooks/use_url_state';
import { SecuritySolutionFlyout, TimelineFlyout } from '../../../flyout';
import { useSecuritySolutionNavigation } from '../../../common/components/navigation/use_security_solution_navigation';
import { TimelineId } from '../../../../common/types/timeline';
import { getTimelineShowStatusByIdSelector } from '../../../timelines/store/selectors';
@ -68,36 +68,39 @@ export const SecuritySolutionTemplateWrapper: React.FC<Omit<KibanaPageTemplatePr
* between EuiPageTemplate and the security solution pages.
*/
return (
<ExpandableFlyoutProvider urlKey={isPreview ? undefined : EXPANDABLE_FLYOUT_URL_KEY}>
<StyledKibanaPageTemplate
theme={euiTheme}
$isShowingTimelineOverlay={isShowingTimelineOverlay}
paddingSize="none"
solutionNav={solutionNavProps}
restrictWidth={false}
{...rest}
<StyledKibanaPageTemplate
theme={euiTheme}
$isShowingTimelineOverlay={isShowingTimelineOverlay}
paddingSize="none"
solutionNav={solutionNavProps}
restrictWidth={false}
{...rest}
>
<GlobalKQLHeader />
<KibanaPageTemplate.Section
className="securityPageWrapper"
data-test-subj="pageContainer"
paddingSize={rest.paddingSize ?? 'l'}
alignment="top"
component="div"
grow={true}
>
<GlobalKQLHeader />
<KibanaPageTemplate.Section
className="securityPageWrapper"
data-test-subj="pageContainer"
paddingSize={rest.paddingSize ?? 'l'}
alignment="top"
component="div"
grow={true}
>
<ExpandableFlyoutProvider urlKey={isPreview ? undefined : URL_PARAM_KEY.eventFlyout}>
{children}
</KibanaPageTemplate.Section>
{isTimelineBottomBarVisible && (
<KibanaPageTemplate.BottomBar data-test-subj="timeline-bottom-bar-container">
<EuiThemeProvider colorMode={globalColorMode}>
<SecuritySolutionFlyout />
</ExpandableFlyoutProvider>
</KibanaPageTemplate.Section>
{isTimelineBottomBarVisible && (
<KibanaPageTemplate.BottomBar data-test-subj="timeline-bottom-bar-container">
<EuiThemeProvider colorMode={globalColorMode}>
<ExpandableFlyoutProvider urlKey={URL_PARAM_KEY.timelineFlyout}>
<Timeline />
</EuiThemeProvider>
</KibanaPageTemplate.BottomBar>
)}
<SecuritySolutionFlyout />
</StyledKibanaPageTemplate>
</ExpandableFlyoutProvider>
<TimelineFlyout />
</ExpandableFlyoutProvider>
</EuiThemeProvider>
</KibanaPageTemplate.BottomBar>
)}
</StyledKibanaPageTemplate>
);
});

View file

@ -15,8 +15,6 @@ import { useQueryTimelineByIdOnUrlChange } from './timeline/use_query_timeline_b
import { useInitFlyoutFromUrlParam } from './flyout/use_init_flyout_url_param';
import { useSyncFlyoutUrlParam } from './flyout/use_sync_flyout_url_param';
export const EXPANDABLE_FLYOUT_URL_KEY = 'eventFlyout' as const;
export const useUrlState = () => {
useSyncGlobalQueryString();
useInitSearchBarFromUrlParams();
@ -31,7 +29,8 @@ export const useUrlState = () => {
export const URL_PARAM_KEY = {
appQuery: 'query',
eventFlyout: EXPANDABLE_FLYOUT_URL_KEY,
eventFlyout: 'eventFlyout',
timelineFlyout: 'timelineFlyout',
filters: 'filters',
savedQuery: 'savedQuery',
sourcerer: 'sourcerer',

View file

@ -23,6 +23,7 @@ import {
EuiSkeletonText,
EuiCallOut,
EuiText,
useEuiTheme,
} from '@elastic/eui';
import { ENDPOINT_LIST_ID } from '@kbn/securitysolution-list-constants';
@ -114,6 +115,7 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({
onCancel,
onConfirm,
}: AddExceptionFlyoutProps) {
const { euiTheme } = useEuiTheme();
const { isLoading, indexPatterns, getExtendedFields } = useFetchIndexPatterns(rules);
const [isSubmitting, submitNewExceptionItems] = useAddNewExceptionItems();
const [isClosingAlerts, closeAlerts] = useCloseAlertsFromExceptions();
@ -493,7 +495,13 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({
}, [listType]);
return (
<EuiFlyout size="l" onClose={handleCloseFlyout} data-test-subj="addExceptionFlyout">
<EuiFlyout
size="l"
onClose={handleCloseFlyout}
data-test-subj="addExceptionFlyout"
// EUI TODO: This z-index override of EuiOverlayMask is a workaround, and ideally should be resolved with a cleaner UI/UX flow long-term
maskProps={{ style: `z-index: ${(euiTheme.levels.flyout as number) + 3}` }} // we need this flyout to be above the timeline flyout (which has a z-index of 1002)
>
<FlyoutHeader>
<EuiTitle>
<h2 data-test-subj="exceptionFlyoutTitle">{addExceptionMessage}</h2>

View file

@ -6,8 +6,7 @@
*/
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { copyToClipboard } from '@elastic/eui';
import { render } from '@testing-library/react';
import { RightPanelContext } from '../context';
import { SHARE_BUTTON_TEST_ID, CHAT_BUTTON_TEST_ID } from './test_ids';
import { HeaderActions } from './header_actions';
@ -16,7 +15,6 @@ import { mockGetFieldsData } from '../../shared/mocks/mock_get_fields_data';
import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_formatted_for_field_browser';
import { TestProvidersComponent } from '../../../../common/mock';
import { useGetAlertDetailsFlyoutLink } from '../../../../timelines/components/side_panel/event_details/use_get_alert_details_flyout_link';
import { URL_PARAM_KEY } from '../../../../common/hooks/use_url_state';
jest.mock('../../../../common/lib/kibana');
jest.mock('../hooks/use_assistant');
@ -26,7 +24,6 @@ jest.mock(
jest.mock('@elastic/eui', () => ({
...jest.requireActual('@elastic/eui'),
copyToClipboard: jest.fn(),
EuiCopy: jest.fn(({ children: functionAsChild }) => functionAsChild(jest.fn())),
}));
@ -47,30 +44,16 @@ const renderHeaderActions = (contextValue: RightPanelContext) =>
describe('<HeaderAction />', () => {
beforeEach(() => {
window.location.search = '?';
jest.mocked(useGetAlertDetailsFlyoutLink).mockReturnValue(alertUrl);
jest.mocked(useAssistant).mockReturnValue({ showAssistant: true, promptContextId: '' });
});
describe('Share alert url action', () => {
it('should render share button in the title and copy the the value to clipboard if document is an alert', () => {
const syncedFlyoutState = 'flyoutState';
const query = `?${URL_PARAM_KEY.eventFlyout}=${syncedFlyoutState}`;
Object.defineProperty(window, 'location', {
value: {
search: query,
},
});
it('should render share button in the title', () => {
const { getByTestId } = renderHeaderActions(mockContextValue);
const shareButton = getByTestId(SHARE_BUTTON_TEST_ID);
expect(shareButton).toBeInTheDocument();
fireEvent.click(shareButton);
expect(copyToClipboard).toHaveBeenCalledWith(
`${alertUrl}&${URL_PARAM_KEY.eventFlyout}=${syncedFlyoutState}`
);
});
it('should not render share button in the title if alert is missing url info', () => {

View file

@ -10,8 +10,6 @@ import React, { memo } from 'react';
import { EuiButtonIcon, EuiCopy, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { NewChatByTitle } from '@kbn/elastic-assistant';
import { URL_PARAM_KEY } from '../../../../common/hooks/use_url_state';
import { copyFunction } from '../../../shared/utils/copy_to_clipboard';
import { useGetAlertDetailsFlyoutLink } from '../../../../timelines/components/side_panel/event_details/use_get_alert_details_flyout_link';
import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers';
import { useAssistant } from '../hooks/use_assistant';
@ -37,11 +35,6 @@ export const HeaderActions: VFC = memo(() => {
const showShareAlertButton = isAlert && alertDetailsLink;
const modifier = (value: string) => {
const query = new URLSearchParams(window.location.search);
return `${value}&${URL_PARAM_KEY.eventFlyout}=${query.get(URL_PARAM_KEY.eventFlyout)}`;
};
const { showAssistant, promptContextId } = useAssistant({
dataFormattedForFieldBrowser,
isAlert,
@ -84,8 +77,8 @@ export const HeaderActions: VFC = memo(() => {
{ defaultMessage: 'Share alert' }
)}
data-test-subj={SHARE_BUTTON_TEST_ID}
onClick={() => copyFunction(copy, alertDetailsLink, modifier)}
onKeyDown={() => copyFunction(copy, alertDetailsLink, modifier)}
onClick={copy}
onKeyDown={copy}
/>
)}
</EuiCopy>

View file

@ -6,16 +6,14 @@
*/
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { render } from '@testing-library/react';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { copyToClipboard } from '@elastic/eui';
import { RightPanelContext } from '../context';
import { JsonTab } from './json_tab';
import { JSON_TAB_CONTENT_TEST_ID, JSON_TAB_COPY_TO_CLIPBOARD_BUTTON_TEST_ID } from './test_ids';
jest.mock('@elastic/eui', () => ({
...jest.requireActual('@elastic/eui'),
copyToClipboard: jest.fn(),
EuiCopy: jest.fn(({ children: functionAsChild }) => functionAsChild(jest.fn())),
}));
@ -47,9 +45,5 @@ describe('<JsonTab />', () => {
const copyToClipboardButton = getByTestId(JSON_TAB_COPY_TO_CLIPBOARD_BUTTON_TEST_ID);
expect(copyToClipboardButton).toBeInTheDocument();
fireEvent.click(copyToClipboardButton);
expect(copyToClipboard).toHaveBeenCalledWith(JSON.stringify(searchHit, null, 2));
});
});

View file

@ -11,7 +11,6 @@ import { JsonCodeEditor } from '@kbn/unified-doc-viewer-plugin/public';
import { EuiButtonEmpty, EuiCopy, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { copyFunction } from '../../../shared/utils/copy_to_clipboard';
import { JSON_TAB_CONTENT_TEST_ID, JSON_TAB_COPY_TO_CLIPBOARD_BUTTON_TEST_ID } from './test_ids';
import { useRightPanelContext } from '../context';
@ -68,8 +67,8 @@ export const JsonTab: FC = memo(() => {
}
)}
data-test-subj={JSON_TAB_COPY_TO_CLIPBOARD_BUTTON_TEST_ID}
onClick={() => copyFunction(copy, jsonValue)}
onKeyDown={() => copyFunction(copy, jsonValue)}
onClick={copy}
onKeyDown={copy}
>
<FormattedMessage
id="xpack.securitySolution.flyout.right.jsonTab.copyToClipboardButtonLabel"

View file

@ -7,6 +7,7 @@
import React, { memo } from 'react';
import { ExpandableFlyout, type ExpandableFlyoutProps } from '@kbn/expandable-flyout';
import { useEuiTheme } from '@elastic/eui';
import type { IsolateHostPanelProps } from './document_details/isolate_host';
import {
IsolateHostPanel,
@ -90,8 +91,30 @@ const expandableFlyoutDocumentsPanels: ExpandableFlyoutProps['registeredPanels']
},
];
/**
* Flyout used for the Security Solution application
* We keep the default EUI 1000 z-index to ensure it is always rendered behind Timeline (which has a z-index of 1001)
*/
export const SecuritySolutionFlyout = memo(() => (
<ExpandableFlyout registeredPanels={expandableFlyoutDocumentsPanels} paddingSize="none" />
));
SecuritySolutionFlyout.displayName = 'SecuritySolutionFlyout';
/**
* Flyout used in Timeline
* We set the z-index to 1002 to ensure it is always rendered above Timeline (which has a z-index of 1001)
*/
export const TimelineFlyout = memo(() => {
const { euiTheme } = useEuiTheme();
return (
<ExpandableFlyout
registeredPanels={expandableFlyoutDocumentsPanels}
paddingSize="none"
customStyles={{ 'z-index': (euiTheme.levels.flyout as number) + 2 }}
/>
);
});
TimelineFlyout.displayName = 'TimelineFlyout';

View file

@ -1,39 +0,0 @@
/*
* 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 { copyFunction } from './copy_to_clipboard';
jest.mock('@elastic/eui', () => ({
...jest.requireActual('@elastic/eui'),
copyToClipboard: jest.fn(),
EuiCopy: jest.fn(({ children: functionAsChild }) => functionAsChild(jest.fn())),
}));
describe('copyFunction', () => {
beforeEach(() => {
jest.clearAllMocks();
});
const rawValue = 'rawValue';
it('should call copy function', () => {
const euiCopy = jest.fn();
copyFunction(euiCopy, rawValue);
expect(euiCopy).toHaveBeenCalled();
});
it('should call modifier function if passed', () => {
const euiCopy = jest.fn();
const modifiedFc = jest.fn();
copyFunction(euiCopy, rawValue, modifiedFc);
expect(modifiedFc).toHaveBeenCalledWith(rawValue);
});
});

View file

@ -1,31 +0,0 @@
/*
* 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 { copyToClipboard } from '@elastic/eui';
/**
* Copy to clipboard wrapper component. It allows adding a copy to clipboard functionality to any element.
* It expects the value to be copied with an optional function to modify the value if necessary.
*
* @param copy the copy method from EuiCopy
* @param rawValue the value to save to the clipboard
* @param modifier a function to modify the raw value before saving to the clipboard
*/
export const copyFunction = (
copy: Function,
rawValue: string,
modifier?: (rawValue: string) => string
) => {
copy();
if (modifier) {
const modifiedCopyValue = modifier(rawValue);
copyToClipboard(modifiedCopyValue);
} else {
copyToClipboard(rawValue);
}
};

View file

@ -6,13 +6,21 @@
*/
import React, { memo, useCallback, useState } from 'react';
import { EuiButton, EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui';
import {
EuiButton,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutHeader,
EuiTitle,
useEuiTheme,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import type { EndpointResponderExtensionComponentProps } from '../types';
import { ResponseActionsLog } from '../../endpoint_response_actions_list/response_actions_log';
import { UX_MESSAGES } from '../../endpoint_response_actions_list/translations';
export const ActionLogButton = memo<EndpointResponderExtensionComponentProps>((props) => {
const { euiTheme } = useEuiTheme();
const [showActionLogFlyout, setShowActionLogFlyout] = useState<boolean>(false);
const toggleActionLog = useCallback(() => {
setShowActionLogFlyout((prevState) => {
@ -39,6 +47,8 @@ export const ActionLogButton = memo<EndpointResponderExtensionComponentProps>((p
size="m"
paddingSize="l"
data-test-subj="responderActionLogFlyout"
// EUI TODO: This z-index override of EuiOverlayMask is a workaround, and ideally should be resolved with a cleaner UI/UX flow long-term
maskProps={{ style: `z-index: ${(euiTheme.levels.flyout as number) + 3}` }} // we need this flyout to be above the timeline flyout (which has a z-index of 1002)
>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">

View file

@ -31,7 +31,9 @@ const OverlayRootContainer = styled.div`
height: calc(100% - var(--euiFixedHeadersOffset, 0));
width: 100%;
z-index: ${({ theme: { eui } }) => eui.euiZFlyout};
z-index: ${({ theme: { eui } }) =>
eui.euiZFlyout +
3}; // we need to have this response div rendered above the timeline flyout (with z-index at 1002)
background-color: ${({ theme: { eui } }) => eui.euiColorEmptyShade};

View file

@ -9,6 +9,7 @@ import { closeAllToasts } from '../../tasks/toasts';
import {
getAlertsTableRows,
openAlertDetailsView,
openAlertDetailsViewFromTimeline,
openInvestigateInTimelineView,
openResponderFromEndpointAlertDetails,
} from '../../screens/alerts';
@ -104,9 +105,7 @@ describe(
getAlertsTableRows().should('have.length.greaterThan', 0);
openInvestigateInTimelineView();
cy.getByTestSubj('timeline-container').within(() => {
openAlertDetailsView();
});
openAlertDetailsViewFromTimeline();
openResponderFromEndpointAlertDetails();
ensureOnResponder();
});

View file

@ -47,6 +47,13 @@ export const openAlertDetailsView = (rowIndex: number = 0): void => {
cy.getByTestSubj('take-action-dropdown-btn').click();
};
export const openAlertDetailsViewFromTimeline = (rowIndex: number = 0): void => {
cy.getByTestSubj('timeline-container').within(() => {
cy.getByTestSubj('expand-event').eq(rowIndex).click();
});
cy.getByTestSubj('take-action-dropdown-btn').click();
};
export const openInvestigateInTimelineView = (): void => {
cy.getByTestSubj('send-alert-to-timeline-button').first().click();
};

View file

@ -28,7 +28,8 @@ export const usePaneStyles = () => {
right: 0;
bottom: 0;
background: ${transparentize(euiTheme.colors.ink, 0.5)};
z-index: ${euiTheme.levels.flyout};
z-index: ${(euiTheme.levels.flyout as number) +
1}; // this z-index needs to be between the eventFlyout (set at 1000) and the timelineFlyout (set at 1002)
${euiCanAnimate} {
animation: ${euiAnimFadeIn} ${euiTheme.animation.fast} ease-in;

View file

@ -9,6 +9,11 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
import { isEventBuildingBlockType } from '@kbn/securitysolution-data-table';
import { useUiSetting$ } from '@kbn/kibana-react-plugin/public';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
import { DocumentDetailsRightPanelKey } from '../../../../../flyout/document_details/right';
import { ENABLE_EXPANDABLE_FLYOUT_SETTING } from '../../../../../../common/constants';
import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector';
import type {
ColumnHeaderOptions,
@ -102,6 +107,14 @@ const StatefulEventComponent: React.FC<Props> = ({
}) => {
const trGroupRef = useRef<HTMLDivElement | null>(null);
const dispatch = useDispatch();
const expandableTimelineFlyoutEnabled = useIsExperimentalFeatureEnabled(
'expandableTimelineFlyoutEnabled'
);
const { openFlyout } = useExpandableFlyoutApi();
const [isSecurityFlyoutEnabled] = useUiSetting$<boolean>(ENABLE_EXPANDABLE_FLYOUT_SETTING);
// Store context in state rather than creating object in provider value={} to prevent re-renders caused by a new object being created
const [activeStatefulEventContext] = useState({
timelineID: timelineId,
@ -199,14 +212,38 @@ const StatefulEventComponent: React.FC<Props> = ({
},
};
dispatch(
timelineActions.toggleDetailPanel({
...updatedExpandedDetail,
tabType,
id: timelineId,
})
);
}, [dispatch, event._id, event._index, refetch, tabType, timelineId]);
if (isSecurityFlyoutEnabled && expandableTimelineFlyoutEnabled) {
openFlyout({
right: {
id: DocumentDetailsRightPanelKey,
params: {
id: eventId,
indexName,
scopeId: timelineId,
},
},
});
} else {
// opens the panel when clicking on the table row action
dispatch(
timelineActions.toggleDetailPanel({
...updatedExpandedDetail,
tabType,
id: timelineId,
})
);
}
}, [
dispatch,
event._id,
event._index,
expandableTimelineFlyoutEnabled,
isSecurityFlyoutEnabled,
openFlyout,
refetch,
tabType,
timelineId,
]);
const associateNote = useCallback(
(noteId: string) => {

View file

@ -14,14 +14,13 @@ import { TimelineId, TimelineTabs } from '../../../../../../common/types/timelin
import { timelineActions } from '../../../../store';
import { StatefulEventContext } from '../../../../../common/components/events_viewer/stateful_event_context';
import { createTelemetryServiceMock } from '../../../../../common/lib/telemetry/telemetry_service.mock';
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
import { dataTableActions, TableId } from '@kbn/securitysolution-data-table';
const mockedTelemetry = createTelemetryServiceMock();
const mockUseIsExperimentalFeatureEnabled = jest.fn();
const mockOpenRightPanel = jest.fn();
jest.mock('../../../../../common/hooks/use_experimental_features', () => ({
useIsExperimentalFeatureEnabled: () => mockUseIsExperimentalFeatureEnabled,
}));
jest.mock('../../../../../common/hooks/use_experimental_features');
jest.mock('@kbn/expandable-flyout', () => {
return {
@ -69,7 +68,22 @@ jest.mock('../../../../store', () => {
};
});
jest.mock('@kbn/securitysolution-data-table', () => {
const original = jest.requireActual('@kbn/securitysolution-data-table');
return {
...original,
dataTableActions: {
...original.dataTableActions,
toggleDetailPanel: jest.fn(),
},
};
});
describe('HostName', () => {
afterEach(() => {
jest.clearAllMocks();
});
const props = {
fieldName: 'host.name',
contextId: 'test-context-id',
@ -106,7 +120,7 @@ describe('HostName', () => {
expect(wrapper.find('[data-test-subj="DefaultDraggable"]').exists()).toEqual(true);
});
test('if not enableHostDetailsFlyout, should go to hostdetails page', async () => {
test('should not open any flyout or panels if context in not defined', async () => {
const wrapper = mount(
<TestProviders>
<HostName {...props} />
@ -115,11 +129,99 @@ describe('HostName', () => {
wrapper.find('[data-test-subj="host-details-button"]').last().simulate('click');
await waitFor(() => {
expect(dataTableActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(mockOpenRightPanel).not.toHaveBeenCalled();
});
});
test('if enableHostDetailsFlyout, should open HostDetailsSidePanel', async () => {
test('should not open any flyout or panels if enableHostDetailsFlyout is false', async () => {
const context = {
enableHostDetailsFlyout: false,
enableIpDetailsFlyout: true,
timelineID: TimelineId.active,
tabType: TimelineTabs.query,
};
const wrapper = mount(
<TestProviders>
<StatefulEventContext.Provider value={context}>
<HostName {...props} />
</StatefulEventContext.Provider>
</TestProviders>
);
wrapper.find('[data-test-subj="host-details-button"]').last().simulate('click');
await waitFor(() => {
expect(dataTableActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(mockOpenRightPanel).not.toHaveBeenCalled();
});
});
test('should not open any flyout or panels if timelineID is not defined', async () => {
const context = {
enableHostDetailsFlyout: true,
enableIpDetailsFlyout: true,
timelineID: '',
tabType: TimelineTabs.query,
};
const wrapper = mount(
<TestProviders>
<StatefulEventContext.Provider value={context}>
<HostName {...props} />
</StatefulEventContext.Provider>
</TestProviders>
);
wrapper.find('[data-test-subj="host-details-button"]').last().simulate('click');
await waitFor(() => {
expect(dataTableActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(mockOpenRightPanel).not.toHaveBeenCalled();
});
});
test('should open old flyout on table', async () => {
(useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation((feature: string) => {
if (feature === 'newHostDetailsFlyout') return false;
if (feature === 'expandableTimelineFlyoutEnabled') return false;
});
const context = {
enableHostDetailsFlyout: true,
enableIpDetailsFlyout: true,
timelineID: TableId.alertsOnAlertsPage,
tabType: TimelineTabs.query,
};
const wrapper = mount(
<TestProviders>
<StatefulEventContext.Provider value={context}>
<HostName {...props} />
</StatefulEventContext.Provider>
</TestProviders>
);
wrapper.find('[data-test-subj="host-details-button"]').last().simulate('click');
await waitFor(() => {
expect(dataTableActions.toggleDetailPanel).toHaveBeenCalledWith({
id: context.timelineID,
panelView: 'hostDetail',
params: {
hostName: props.value,
},
tabType: context.tabType,
});
expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(mockOpenRightPanel).not.toHaveBeenCalled();
});
});
test('should open old flyout in timeline', async () => {
(useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation((feature: string) => {
if (feature === 'newHostDetailsFlyout') return false;
if (feature === 'expandableTimelineFlyoutEnabled') return false;
});
const context = {
enableHostDetailsFlyout: true,
enableIpDetailsFlyout: true,
@ -144,15 +246,20 @@ describe('HostName', () => {
},
tabType: context.tabType,
});
expect(dataTableActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(mockOpenRightPanel).not.toHaveBeenCalled();
});
});
test('it should open expandable flyout if timeline is not in context and experimental flag is enabled', async () => {
mockUseIsExperimentalFeatureEnabled.mockReturnValue(true);
test('should open expandable flyout on table', async () => {
(useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation((feature: string) => {
if (feature === 'newHostDetailsFlyout') return true;
if (feature === 'expandableTimelineFlyoutEnabled') return false;
});
const context = {
enableHostDetailsFlyout: true,
enableIpDetailsFlyout: true,
timelineID: 'fake-timeline',
timelineID: TableId.alertsOnAlertsPage,
tabType: TimelineTabs.query,
};
const wrapper = mount(
@ -165,7 +272,52 @@ describe('HostName', () => {
wrapper.find('[data-test-subj="host-details-button"]').last().simulate('click');
await waitFor(() => {
expect(mockOpenRightPanel).toHaveBeenCalled();
expect(mockOpenRightPanel).toHaveBeenCalledWith({
id: 'host-panel',
params: {
hostName: props.value,
contextID: props.contextId,
scopeId: TableId.alertsOnAlertsPage,
isDraggable: false,
},
});
expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(dataTableActions.toggleDetailPanel).not.toHaveBeenCalled();
});
});
test('should open expandable flyout in timeline', async () => {
(useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation((feature: string) => {
if (feature === 'newHostDetailsFlyout') return true;
if (feature === 'expandableTimelineFlyoutEnabled') return true;
});
const context = {
enableHostDetailsFlyout: true,
enableIpDetailsFlyout: true,
timelineID: TimelineId.active,
tabType: TimelineTabs.query,
};
const wrapper = mount(
<TestProviders>
<StatefulEventContext.Provider value={context}>
<HostName {...props} />
</StatefulEventContext.Provider>
</TestProviders>
);
wrapper.find('[data-test-subj="host-details-button"]').last().simulate('click');
await waitFor(() => {
expect(mockOpenRightPanel).toHaveBeenCalledWith({
id: 'host-panel',
params: {
hostName: props.value,
contextID: props.contextId,
scopeId: TableId.alertsOnAlertsPage,
isDraggable: false,
},
});
expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(dataTableActions.toggleDetailPanel).not.toHaveBeenCalled();
});
});
});

View file

@ -50,6 +50,9 @@ const HostNameComponent: React.FC<Props> = ({
value,
}) => {
const isNewHostDetailsFlyoutEnabled = useIsExperimentalFeatureEnabled('newHostDetailsFlyout');
const expandableTimelineFlyoutEnabled = useIsExperimentalFeatureEnabled(
'expandableTimelineFlyoutEnabled'
);
const { openRightPanel } = useExpandableFlyoutApi();
const dispatch = useDispatch();
@ -57,6 +60,7 @@ const HostNameComponent: React.FC<Props> = ({
const hostName = `${value}`;
const isInTimelineContext =
hostName && eventContext?.enableHostDetailsFlyout && eventContext?.timelineID;
const openHostDetailsSidePanel = useCallback(
(e) => {
e.preventDefault();
@ -65,49 +69,63 @@ const HostNameComponent: React.FC<Props> = ({
onClick();
}
if (eventContext && isInTimelineContext) {
const { timelineID, tabType } = eventContext;
if (!eventContext || !isInTimelineContext) {
return;
}
if (isNewHostDetailsFlyoutEnabled && !isTimelineScope(timelineID)) {
openRightPanel({
id: HostPanelKey,
params: {
hostName,
contextID: contextId,
scopeId: TableId.alertsOnAlertsPage,
isDraggable,
},
});
} else {
const updatedExpandedDetail: ExpandedDetailType = {
panelView: 'hostDetail',
params: {
hostName,
},
};
const scopedActions = getScopedActions(timelineID);
if (scopedActions) {
dispatch(
scopedActions.toggleDetailPanel({
...updatedExpandedDetail,
id: timelineID,
tabType: tabType as TimelineTabs,
})
);
}
const { timelineID, tabType } = eventContext;
const openNewFlyout = () =>
openRightPanel({
id: HostPanelKey,
params: {
hostName,
contextID: contextId,
scopeId: TableId.alertsOnAlertsPage,
isDraggable,
},
});
const openOldFlyout = () => {
const updatedExpandedDetail: ExpandedDetailType = {
panelView: 'hostDetail',
params: {
hostName,
},
};
const scopedActions = getScopedActions(timelineID);
if (scopedActions) {
dispatch(
scopedActions.toggleDetailPanel({
...updatedExpandedDetail,
id: timelineID,
tabType: tabType as TimelineTabs,
})
);
}
};
if (
(isTimelineScope(timelineID) &&
isNewHostDetailsFlyoutEnabled &&
expandableTimelineFlyoutEnabled) ||
isNewHostDetailsFlyoutEnabled
) {
openNewFlyout();
} else {
openOldFlyout();
}
},
[
onClick,
contextId,
dispatch,
eventContext,
expandableTimelineFlyoutEnabled,
hostName,
isDraggable,
isInTimelineContext,
isNewHostDetailsFlyoutEnabled,
onClick,
openRightPanel,
hostName,
contextId,
isDraggable,
dispatch,
]
);

View file

@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import React, { type PropsWithChildren } from 'react';
import { mount } from 'enzyme';
import { waitFor } from '@testing-library/react';
@ -14,8 +14,22 @@ import { timelineActions } from '../../../../store';
import { UserName } from './user_name';
import { StatefulEventContext } from '../../../../../common/components/events_viewer/stateful_event_context';
import { createTelemetryServiceMock } from '../../../../../common/lib/telemetry/telemetry_service.mock';
import { dataTableActions, TableId } from '@kbn/securitysolution-data-table';
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
const mockedTelemetry = createTelemetryServiceMock();
const mockOpenRightPanel = jest.fn();
jest.mock('../../../../../common/hooks/use_experimental_features');
jest.mock('@kbn/expandable-flyout', () => {
return {
useExpandableFlyoutApi: () => ({
openRightPanel: mockOpenRightPanel,
}),
TestProvider: ({ children }: PropsWithChildren<{}>) => <>{children}</>,
};
});
jest.mock('react-redux', () => {
const origin = jest.requireActual('react-redux');
@ -54,7 +68,22 @@ jest.mock('../../../../store', () => {
};
});
jest.mock('@kbn/securitysolution-data-table', () => {
const original = jest.requireActual('@kbn/securitysolution-data-table');
return {
...original,
dataTableActions: {
...original.dataTableActions,
toggleDetailPanel: jest.fn(),
},
};
});
describe('UserName', () => {
afterEach(() => {
jest.clearAllMocks();
});
const props = {
fieldName: 'user.name',
fieldType: 'keyword',
@ -89,11 +118,88 @@ describe('UserName', () => {
expect(wrapper.find('[data-test-subj="DefaultDraggable"]').exists()).toEqual(true);
});
test('if newUserDetailsFlyout, should open UserDetailsSidePanel', async () => {
test('should not open any flyout or panels if context in not defined', async () => {
const wrapper = mount(
<TestProviders>
<UserName {...props} />
</TestProviders>
);
wrapper.find('[data-test-subj="users-link-anchor"]').last().simulate('click');
await waitFor(() => {
expect(dataTableActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(mockOpenRightPanel).not.toHaveBeenCalled();
});
});
test('should not open any flyout or panels if timelineID is not defined', async () => {
const context = {
enableHostDetailsFlyout: true,
enableIpDetailsFlyout: true,
timelineID: TimelineId.test,
timelineID: '',
tabType: TimelineTabs.query,
};
const wrapper = mount(
<TestProviders>
<StatefulEventContext.Provider value={context}>
<UserName {...props} />
</StatefulEventContext.Provider>
</TestProviders>
);
wrapper.find('[data-test-subj="users-link-anchor"]').last().simulate('click');
await waitFor(() => {
expect(dataTableActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(mockOpenRightPanel).not.toHaveBeenCalled();
});
});
test('should open old flyout on table', async () => {
(useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation((feature: string) => {
if (feature === 'newUserDetailsFlyout') return false;
if (feature === 'expandableTimelineFlyoutEnabled') return false;
});
const context = {
enableHostDetailsFlyout: true,
enableIpDetailsFlyout: true,
timelineID: TableId.alertsOnAlertsPage,
tabType: TimelineTabs.query,
};
const wrapper = mount(
<TestProviders>
<StatefulEventContext.Provider value={context}>
<UserName {...props} />
</StatefulEventContext.Provider>
</TestProviders>
);
wrapper.find('[data-test-subj="users-link-anchor"]').last().simulate('click');
await waitFor(() => {
expect(dataTableActions.toggleDetailPanel).toHaveBeenCalledWith({
id: context.timelineID,
panelView: 'userDetail',
params: {
userName: props.value,
},
tabType: context.tabType,
});
expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(mockOpenRightPanel).not.toHaveBeenCalled();
});
});
test('should open old flyout in timeline', async () => {
(useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation((feature: string) => {
if (feature === 'newUserDetailsFlyout') return false;
if (feature === 'expandableTimelineFlyoutEnabled') return false;
});
const context = {
enableHostDetailsFlyout: true,
enableIpDetailsFlyout: true,
timelineID: TimelineId.active,
tabType: TimelineTabs.query,
};
const wrapper = mount(
@ -114,6 +220,78 @@ describe('UserName', () => {
},
tabType: context.tabType,
});
expect(dataTableActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(mockOpenRightPanel).not.toHaveBeenCalled();
});
});
test('should open expandable flyout on table', async () => {
(useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation((feature: string) => {
if (feature === 'newUserDetailsFlyout') return true;
if (feature === 'expandableTimelineFlyoutEnabled') return false;
});
const context = {
enableHostDetailsFlyout: true,
enableIpDetailsFlyout: true,
timelineID: TableId.alertsOnAlertsPage,
tabType: TimelineTabs.query,
};
const wrapper = mount(
<TestProviders>
<StatefulEventContext.Provider value={context}>
<UserName {...props} />
</StatefulEventContext.Provider>
</TestProviders>
);
wrapper.find('[data-test-subj="users-link-anchor"]').last().simulate('click');
await waitFor(() => {
expect(mockOpenRightPanel).toHaveBeenCalledWith({
id: 'user-panel',
params: {
userName: props.value,
contextID: props.contextId,
scopeId: TableId.alertsOnAlertsPage,
isDraggable: false,
},
});
expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(dataTableActions.toggleDetailPanel).not.toHaveBeenCalled();
});
});
test('should open expandable flyout in timeline', async () => {
(useIsExperimentalFeatureEnabled as jest.Mock).mockImplementation((feature: string) => {
if (feature === 'newUserDetailsFlyout') return true;
if (feature === 'expandableTimelineFlyoutEnabled') return true;
});
const context = {
enableHostDetailsFlyout: true,
enableIpDetailsFlyout: true,
timelineID: TimelineId.active,
tabType: TimelineTabs.query,
};
const wrapper = mount(
<TestProviders>
<StatefulEventContext.Provider value={context}>
<UserName {...props} />
</StatefulEventContext.Provider>
</TestProviders>
);
wrapper.find('[data-test-subj="users-link-anchor"]').last().simulate('click');
await waitFor(() => {
expect(mockOpenRightPanel).toHaveBeenCalledWith({
id: 'user-panel',
params: {
userName: props.value,
contextID: props.contextId,
scopeId: TableId.alertsOnAlertsPage,
isDraggable: false,
},
});
expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled();
expect(dataTableActions.toggleDetailPanel).not.toHaveBeenCalled();
});
});
});

View file

@ -52,6 +52,9 @@ const UserNameComponent: React.FC<Props> = ({
const dispatch = useDispatch();
const eventContext = useContext(StatefulEventContext);
const isNewUserDetailsFlyoutEnable = useIsExperimentalFeatureEnabled('newUserDetailsFlyout');
const expandableTimelineFlyoutEnabled = useIsExperimentalFeatureEnabled(
'expandableTimelineFlyoutEnabled'
);
const userName = `${value}`;
const isInTimelineContext = userName && eventContext?.timelineID;
const { openRightPanel } = useExpandableFlyoutApi();
@ -64,49 +67,64 @@ const UserNameComponent: React.FC<Props> = ({
onClick();
}
if (eventContext && isInTimelineContext) {
const { timelineID, tabType } = eventContext;
if (!eventContext || !isInTimelineContext) {
return;
}
if (isNewUserDetailsFlyoutEnable && !isTimelineScope(timelineID)) {
openRightPanel({
id: UserPanelKey,
params: {
userName,
contextID: contextId,
scopeId: TableId.alertsOnAlertsPage,
isDraggable,
},
});
} else {
const updatedExpandedDetail: ExpandedDetailType = {
panelView: 'userDetail',
params: {
userName,
},
};
const scopedActions = getScopedActions(timelineID);
if (scopedActions) {
dispatch(
scopedActions.toggleDetailPanel({
...updatedExpandedDetail,
id: timelineID,
tabType: tabType as TimelineTabs,
})
);
}
const { timelineID, tabType } = eventContext;
const openNewFlyout = () =>
openRightPanel({
id: UserPanelKey,
params: {
userName,
contextID: contextId,
scopeId: TableId.alertsOnAlertsPage,
isDraggable,
},
});
const openOldFlyout = () => {
const updatedExpandedDetail: ExpandedDetailType = {
panelView: 'userDetail',
params: {
userName,
},
};
const scopedActions = getScopedActions(timelineID);
if (scopedActions) {
dispatch(
scopedActions.toggleDetailPanel({
...updatedExpandedDetail,
id: timelineID,
tabType: tabType as TimelineTabs,
})
);
}
};
if (
(isTimelineScope(timelineID) &&
isNewUserDetailsFlyoutEnable &&
expandableTimelineFlyoutEnabled) ||
isNewUserDetailsFlyoutEnable
) {
openNewFlyout();
} else {
openOldFlyout();
}
},
[
onClick,
contextId,
dispatch,
eventContext,
isNewUserDetailsFlyoutEnable,
expandableTimelineFlyoutEnabled,
isDraggable,
isInTimelineContext,
isNewUserDetailsFlyoutEnable,
onClick,
openRightPanel,
userName,
contextId,
isDraggable,
dispatch,
]
);

View file

@ -1,47 +0,0 @@
/*
* 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 { ID_HEADER_FIELD, TIMESTAMP_HEADER_FIELD } from '../../../screens/timeline';
import { login } from '../../../tasks/login';
import { visitWithTimeRange } from '../../../tasks/navigation';
import { openTimelineUsingToggle } from '../../../tasks/security_main';
import {
clickIdToggleField,
expandFirstTimelineEventDetails,
populateTimeline,
clickTimestampToggleField,
} from '../../../tasks/timeline';
import { hostsUrl } from '../../../urls/navigation';
describe('toggle column in timeline', { tags: ['@ess', '@serverless'] }, () => {
before(() => {
cy.intercept('POST', '/api/timeline/_export?file_name=timelines_export.ndjson').as('export');
});
beforeEach(() => {
login();
visitWithTimeRange(hostsUrl('allHosts'));
openTimelineUsingToggle();
populateTimeline();
});
it('removes the @timestamp field from the timeline when the user un-checks the toggle', () => {
expandFirstTimelineEventDetails();
clickTimestampToggleField();
cy.get(TIMESTAMP_HEADER_FIELD).should('not.exist');
});
it('adds the _id field to the timeline when the user checks the field', () => {
expandFirstTimelineEventDetails();
clickIdToggleField();
cy.get(ID_HEADER_FIELD).should('exist');
});
});