[Security Solution] Expandable flyout - right panel header refactor (#170279)

This commit is contained in:
christineweng 2023-11-06 09:28:40 -06:00 committed by GitHub
parent 5e8414779a
commit 3bf58b04ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 975 additions and 575 deletions

View file

@ -111,7 +111,7 @@ describe('Alert Flyout Automated Action Results', () => {
});
});
cy.contains(timelineRegex);
cy.getBySel('securitySolutionFlyoutHeaderCollapseDetailButton').click();
cy.getBySel('securitySolutionFlyoutNavigationCollapseDetailButton').click();
cy.getBySel('flyoutBottomBar').contains('Untitled timeline').click();
cy.contains(filterRegex);
});

View file

@ -8,7 +8,7 @@ The Security Solution plugin aims at having a single instance of the expandable
> Remember to add any new panels to the `index.tsx` at the root of the [flyout folder](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/flyout). These are passed to the `@kbn/expandable-flyout` package as `registeredPanels`. Failing to do so will result in the panel not being rendered.
## Notes
## Folder Structure
The structure of the `flyout` folder is intended to work as follows:
- multiple top level folders referring to the _type_ of flyout (for example document details, user, host, rule, cases...) and would contain all the panels for that flyout _type_. Each of these top level folders can be organized the way you want, but we recommend following a similar structure to the one we have for the `document_details` flyout type, where the `right`, `left` and `preview` folders correspond to the panels displayed in the right, left and preview flyout sections respectively. The `shared` folder contains any shared components/hooks/services/helpers that are used within the other folders.
@ -49,3 +49,15 @@ flyout
└─── shared
└─── components
```
## Shared flyout components
Here's a non-exhaustive list of the reusable component in the top-level `shared` folder. We recommend using these components to create a unified flyout experience.
- [FlyoutNavigation](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_navigation.tsx): navigation menu on the **right panel** only, with expand/collapse button and option to pass in a list of actions to be displayed on top. Works best when used in combination with the header component below.
- [FlyoutHeader](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_header.tsx): wrapper of `EuiFlyoutHeader`, setting the recommended `16px` padding using a EuiPanel.
- [FlyoutHeaderTabs](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_header_tabs.tsx): Wrapper of `EuiTabs`, setting bottom margin to align with the flyout header divider
- [FlyoutBody](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_body.tsx): wrapper of `EuiFlyoutHeader`, setting the recommended `16px` padding using a EuiPanel.
- [FlyoutFooter](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_footer.tsx): wrapper of `EuiFlyoutFooter`, setting the recommended `16px` padding using a EuiPanel.
- [FlyoutError](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_error.tsx): displays a `EuiEmptyPrompt` for error messages, correctly positioned and sized when used in at the panel level (not for individual components)
- [FlyoutLoading](https://github.com/elastic/kibana/tree/main/x-pack/plugins/security_solution/public/flyout/shared/components/flyout_loading.tsx): displays an `EuiLoadingSpinner` component correctly positioned and sized when used in at the panel level (not for individual components)

View file

@ -8,13 +8,13 @@
import type { FC } from 'react';
import React, { useCallback } from 'react';
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
import { EuiPanel } from '@elastic/eui';
import { RightPanelKey } from '../right';
import { useBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers';
import { EndpointIsolateSuccess } from '../../../common/components/endpoint/host_isolation';
import { useHostIsolationTools } from '../../../timelines/components/side_panel/event_details/use_host_isolation_tools';
import { useIsolateHostPanelContext } from './context';
import { HostIsolationPanel } from '../../../detections/components/host_isolation';
import { FlyoutBody } from '../../shared/components/flyout_body';
/**
* Document details expandable flyout section content for the isolate host component, displaying the form or the success banner
@ -43,7 +43,7 @@ export const PanelContent: FC = () => {
);
return (
<EuiPanel hasShadow={false} hasBorder={false}>
<FlyoutBody>
{isIsolateActionSuccessBannerVisible && (
<EndpointIsolateSuccess
hostName={hostName}
@ -57,6 +57,6 @@ export const PanelContent: FC = () => {
successCallback={handleIsolationActionSuccess}
isolateAction={isolateAction}
/>
</EuiPanel>
</FlyoutBody>
);
};

View file

@ -5,12 +5,13 @@
* 2.0.
*/
import { EuiFlyoutHeader, EuiTitle } from '@elastic/eui';
import { EuiTitle } from '@elastic/eui';
import type { FC } from 'react';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { useIsolateHostPanelContext } from './context';
import { FLYOUT_HEADER_TITLE_TEST_ID } from './test_ids';
import { FlyoutHeader } from '../../shared/components/flyout_header';
/**
* Document details expandable right section header for the isolate host panel
@ -32,10 +33,10 @@ export const PanelHeader: FC = () => {
);
return (
<EuiFlyoutHeader hasBorder>
<FlyoutHeader>
<EuiTitle size="s">
<h4 data-test-subj={FLYOUT_HEADER_TITLE_TEST_ID}>{title}</h4>
</EuiTitle>
</EuiFlyoutHeader>
</FlyoutHeader>
);
};

View file

@ -5,12 +5,13 @@
* 2.0.
*/
import { EuiFlyoutBody, useEuiBackgroundColor } from '@elastic/eui';
import { useEuiBackgroundColor } from '@elastic/eui';
import type { VFC } from 'react';
import React, { useMemo } from 'react';
import { css } from '@emotion/react';
import type { LeftPanelPaths } from '.';
import { tabs } from './tabs';
import { FlyoutBody } from '../../shared/components/flyout_body';
export interface PanelContentProps {
/**
@ -29,13 +30,13 @@ export const PanelContent: VFC<PanelContentProps> = ({ selectedTabId }) => {
}, [selectedTabId]);
return (
<EuiFlyoutBody
<FlyoutBody
css={css`
background-color: ${useEuiBackgroundColor('subdued')};
`}
>
{selectedTabContent}
</EuiFlyoutBody>
</FlyoutBody>
);
};

View file

@ -5,12 +5,13 @@
* 2.0.
*/
import { EuiFlyoutHeader, EuiTab, EuiTabs, useEuiBackgroundColor } from '@elastic/eui';
import { EuiTab, EuiTabs, useEuiBackgroundColor } from '@elastic/eui';
import type { VFC } from 'react';
import React, { memo } from 'react';
import { css } from '@emotion/react';
import type { LeftPanelPaths } from '.';
import { tabs } from './tabs';
import { FlyoutHeader } from '../../shared/components/flyout_header';
export interface PanelHeaderProps {
/**
@ -44,8 +45,7 @@ export const PanelHeader: VFC<PanelHeaderProps> = memo(({ selectedTabId, setSele
));
return (
<EuiFlyoutHeader
hasBorder
<FlyoutHeader
css={css`
background-color: ${useEuiBackgroundColor('subdued')};
padding-bottom: 0 !important;
@ -55,7 +55,7 @@ export const PanelHeader: VFC<PanelHeaderProps> = memo(({ selectedTabId, setSele
<EuiTabs size="l" expand>
{renderTabs}
</EuiTabs>
</EuiFlyoutHeader>
</FlyoutHeader>
);
});

View file

@ -6,11 +6,12 @@
*/
import React, { memo } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiFlyoutFooter } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { usePreviewPanelContext } from '../context';
import { RenderRuleName } from '../../../../timelines/components/timeline/body/renderers/formatted_field_helpers';
import { SIGNAL_RULE_NAME_FIELD_NAME } from '../../../../timelines/components/timeline/body/renderers/constants';
import { FlyoutFooter } from '../../../shared/components/flyout_footer';
import { RULE_PREVIEW_FOOTER_TEST_ID } from './test_ids';
/**
@ -20,7 +21,7 @@ export const RulePreviewFooter: React.FC = memo(() => {
const { scopeId, eventId, ruleId } = usePreviewPanelContext();
return ruleId ? (
<EuiFlyoutFooter data-test-subj={RULE_PREVIEW_FOOTER_TEST_ID}>
<FlyoutFooter data-test-subj={RULE_PREVIEW_FOOTER_TEST_ID}>
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>
<RenderRuleName
@ -38,7 +39,7 @@ export const RulePreviewFooter: React.FC = memo(() => {
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</FlyoutFooter>
) : null;
});

View file

@ -1,54 +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 React from 'react';
import type { Story } from '@storybook/react';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { ExpandDetailButton } from './expand_detail_button';
import { RightPanelContext } from '../context';
export default {
component: ExpandDetailButton,
title: 'Flyout/ExpandDetailButton',
};
export const Expand: Story<void> = () => {
const flyoutContextValue = {
openLeftPanel: () => window.alert('openLeftPanel called'),
panels: {},
} as unknown as ExpandableFlyoutContext;
const panelContextValue = {
eventId: 'eventId',
indexName: 'indexName',
} as unknown as RightPanelContext;
return (
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
<RightPanelContext.Provider value={panelContextValue}>
<ExpandDetailButton />
</RightPanelContext.Provider>
</ExpandableFlyoutContext.Provider>
);
};
export const Collapse: Story<void> = () => {
const flyoutContextValue = {
closeLeftPanel: () => window.alert('closeLeftPanel called'),
panels: {
left: {},
},
} as unknown as ExpandableFlyoutContext;
const panelContextValue = {} as unknown as RightPanelContext;
return (
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
<RightPanelContext.Provider value={panelContextValue}>
<ExpandDetailButton />
</RightPanelContext.Provider>
</ExpandableFlyoutContext.Provider>
);
};

View file

@ -1,84 +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 React from 'react';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { render } from '@testing-library/react';
import { RightPanelContext } from '../context';
import { ExpandDetailButton } from './expand_detail_button';
import { COLLAPSE_DETAILS_BUTTON_TEST_ID, EXPAND_DETAILS_BUTTON_TEST_ID } from './test_ids';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { LeftPanelKey } from '../../left';
const renderExpandDetailButton = (
flyoutContextValue: ExpandableFlyoutContext,
panelContextValue: RightPanelContext
) =>
render(
<IntlProvider locale="en">
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
<RightPanelContext.Provider value={panelContextValue}>
<ExpandDetailButton />
</RightPanelContext.Provider>
</ExpandableFlyoutContext.Provider>
</IntlProvider>
);
describe('<ExpandDetailButton />', () => {
it('should render expand button', () => {
const flyoutContextValue = {
openLeftPanel: jest.fn(),
panels: {},
} as unknown as ExpandableFlyoutContext;
const panelContextValue = {
eventId: 'eventId',
indexName: 'indexName',
scopeId: 'scopeId',
} as unknown as RightPanelContext;
const { getByTestId, queryByTestId } = renderExpandDetailButton(
flyoutContextValue,
panelContextValue
);
expect(getByTestId(EXPAND_DETAILS_BUTTON_TEST_ID)).toBeInTheDocument();
expect(getByTestId(EXPAND_DETAILS_BUTTON_TEST_ID)).toHaveTextContent('Expand details');
expect(queryByTestId(COLLAPSE_DETAILS_BUTTON_TEST_ID)).not.toBeInTheDocument();
getByTestId(EXPAND_DETAILS_BUTTON_TEST_ID).click();
expect(flyoutContextValue.openLeftPanel).toHaveBeenCalledWith({
id: LeftPanelKey,
params: {
id: panelContextValue.eventId,
indexName: panelContextValue.indexName,
scopeId: panelContextValue.scopeId,
},
});
});
it('should render collapse button', () => {
const flyoutContextValue = {
closeLeftPanel: jest.fn(),
panels: {
left: {},
},
} as unknown as ExpandableFlyoutContext;
const panelContextValue = {} as unknown as RightPanelContext;
const { getByTestId, queryByTestId } = renderExpandDetailButton(
flyoutContextValue,
panelContextValue
);
expect(getByTestId(COLLAPSE_DETAILS_BUTTON_TEST_ID)).toBeInTheDocument();
expect(getByTestId(COLLAPSE_DETAILS_BUTTON_TEST_ID)).toHaveTextContent('Collapse details');
expect(queryByTestId(EXPAND_DETAILS_BUTTON_TEST_ID)).not.toBeInTheDocument();
getByTestId(COLLAPSE_DETAILS_BUTTON_TEST_ID).click();
expect(flyoutContextValue.closeLeftPanel).toHaveBeenCalled();
});
});

View file

@ -1,79 +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 { EuiButtonEmpty } from '@elastic/eui';
import type { FC } from 'react';
import React, { memo, useCallback } from 'react';
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { COLLAPSE_DETAILS_BUTTON_TEST_ID, EXPAND_DETAILS_BUTTON_TEST_ID } from './test_ids';
import { LeftPanelKey } from '../../left';
import { useRightPanelContext } from '../context';
/**
* Button displayed in the top left corner of the panel, to expand the left section of the document details expandable flyout
*/
export const ExpandDetailButton: FC = memo(() => {
const { closeLeftPanel, openLeftPanel, panels } = useExpandableFlyoutContext();
const isExpanded: boolean = panels.left != null;
const { eventId, indexName, scopeId } = useRightPanelContext();
const expandDetails = useCallback(() => {
openLeftPanel({
id: LeftPanelKey,
params: {
id: eventId,
indexName,
scopeId,
},
});
}, [eventId, openLeftPanel, indexName, scopeId]);
const collapseDetails = useCallback(() => closeLeftPanel(), [closeLeftPanel]);
return isExpanded ? (
<EuiButtonEmpty
iconSide="left"
onClick={collapseDetails}
iconType="arrowEnd"
data-test-subj={COLLAPSE_DETAILS_BUTTON_TEST_ID}
aria-label={i18n.translate(
'xpack.securitySolution.flyout.right.header.collapseDetailButtonAriaLabel',
{
defaultMessage: 'Collapse details',
}
)}
>
<FormattedMessage
id="xpack.securitySolution.flyout.right.header.collapseDetailButtonLabel"
defaultMessage="Collapse details"
/>
</EuiButtonEmpty>
) : (
<EuiButtonEmpty
iconSide="left"
onClick={expandDetails}
iconType="arrowStart"
data-test-subj={EXPAND_DETAILS_BUTTON_TEST_ID}
aria-label={i18n.translate(
'xpack.securitySolution.flyout.right.header.expandDetailButtonAriaLabel',
{
defaultMessage: 'Expand details',
}
)}
>
<FormattedMessage
id="xpack.securitySolution.flyout.right.header.expandDetailButtonLabel"
defaultMessage="Expand details"
/>
</EuiButtonEmpty>
);
});
ExpandDetailButton.displayName = 'ExpandDetailButton';

View file

@ -0,0 +1,91 @@
/*
* 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 { render, fireEvent } from '@testing-library/react';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { copyToClipboard } from '@elastic/eui';
import { RightPanelContext } from '../context';
import { SHARE_BUTTON_TEST_ID } from './test_ids';
import { HeaderActions } from './header_actions';
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 { FLYOUT_URL_PARAM } from '../../shared/hooks/url/use_sync_flyout_state_with_url';
jest.mock('../../../../common/lib/kibana');
jest.mock(
'../../../../timelines/components/side_panel/event_details/use_get_alert_details_flyout_link'
);
jest.mock('@elastic/eui', () => ({
...jest.requireActual('@elastic/eui'),
copyToClipboard: jest.fn(),
EuiCopy: jest.fn(({ children: functionAsChild }) => functionAsChild(jest.fn())),
}));
const alertUrl = 'https://example.com/alert';
const flyoutContextValue = {} as unknown as ExpandableFlyoutContext;
const mockContextValue = {
dataFormattedForFieldBrowser: mockDataFormattedForFieldBrowser,
getFieldsData: jest.fn().mockImplementation(mockGetFieldsData),
} as unknown as RightPanelContext;
const renderHeaderActions = (contextValue: RightPanelContext) =>
render(
<TestProvidersComponent>
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
<RightPanelContext.Provider value={contextValue}>
<HeaderActions />
</RightPanelContext.Provider>
</ExpandableFlyoutContext.Provider>
</TestProvidersComponent>
);
describe('<HeaderAction />', () => {
beforeEach(() => {
jest.mocked(useGetAlertDetailsFlyoutLink).mockReturnValue(alertUrl);
});
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 = `?${FLYOUT_URL_PARAM}=${syncedFlyoutState}`;
Object.defineProperty(window, 'location', {
value: {
search: query,
},
});
const { getByTestId } = renderHeaderActions(mockContextValue);
const shareButton = getByTestId(SHARE_BUTTON_TEST_ID);
expect(shareButton).toBeInTheDocument();
fireEvent.click(shareButton);
expect(copyToClipboard).toHaveBeenCalledWith(
`${alertUrl}&${FLYOUT_URL_PARAM}=${syncedFlyoutState}`
);
});
it('should not render share button in the title if alert is missing url info', () => {
jest.mocked(useGetAlertDetailsFlyoutLink).mockReturnValue(null);
const { queryByTestId } = renderHeaderActions(mockContextValue);
expect(queryByTestId(SHARE_BUTTON_TEST_ID)).not.toBeInTheDocument();
});
it('should not render share button in the title if document is not an alert', () => {
const { queryByTestId } = renderHeaderActions({
...mockContextValue,
dataFormattedForFieldBrowser: [],
});
expect(queryByTestId(SHARE_BUTTON_TEST_ID)).not.toBeInTheDocument();
});
});
});

View file

@ -0,0 +1,60 @@
/*
* 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 type { VFC } from 'react';
import React, { memo } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FLYOUT_URL_PARAM } from '../../shared/hooks/url/use_sync_flyout_state_with_url';
import { CopyToClipboard } from '../../../shared/components/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 { useRightPanelContext } from '../context';
import { SHARE_BUTTON_TEST_ID } from './test_ids';
/**
* Actions displayed in the header menu in the right section of alerts flyout
*/
export const HeaderActions: VFC = memo(() => {
const { dataFormattedForFieldBrowser, eventId, indexName } = useRightPanelContext();
const { isAlert, timestamp } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser);
const alertDetailsLink = useGetAlertDetailsFlyoutLink({
_id: eventId,
_index: indexName,
timestamp,
});
const showShareAlertButton = isAlert && alertDetailsLink;
return (
<EuiFlexGroup direction="row" justifyContent="flexEnd">
{showShareAlertButton && (
<EuiFlexItem grow={false}>
<CopyToClipboard
rawValue={alertDetailsLink}
modifier={(value: string) => {
const query = new URLSearchParams(window.location.search);
return `${value}&${FLYOUT_URL_PARAM}=${query.get(FLYOUT_URL_PARAM)}`;
}}
iconType={'share'}
color={'text'}
ariaLabel={i18n.translate(
'xpack.securitySolution.flyout.right.header.shareButtonAriaLabel',
{
defaultMessage: 'Share Alert',
}
)}
data-test-subj={SHARE_BUTTON_TEST_ID}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
);
});
HeaderActions.displayName = 'HeaderActions';

View file

@ -6,15 +6,12 @@
*/
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { render } from '@testing-library/react';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { copyToClipboard } from '@elastic/eui';
import { RightPanelContext } from '../context';
import {
CHAT_BUTTON_TEST_ID,
RISK_SCORE_VALUE_TEST_ID,
SEVERITY_TITLE_TEST_ID,
SHARE_BUTTON_TEST_ID,
SEVERITY_VALUE_TEST_ID,
FLYOUT_HEADER_TITLE_TEST_ID,
} from './test_ids';
import { HeaderTitle } from './header_title';
@ -22,27 +19,13 @@ import moment from 'moment-timezone';
import { useDateFormat, useTimeZone } from '../../../../common/lib/kibana';
import { mockGetFieldsData } from '../../shared/mocks/mock_get_fields_data';
import { mockDataFormattedForFieldBrowser } from '../../shared/mocks/mock_data_formatted_for_field_browser';
import { useAssistant } from '../hooks/use_assistant';
import { TestProvidersComponent } from '../../../../common/mock';
import { useGetAlertDetailsFlyoutLink } from '../../../../timelines/components/side_panel/event_details/use_get_alert_details_flyout_link';
import { FLYOUT_URL_PARAM } from '../../shared/hooks/url/use_sync_flyout_state_with_url';
jest.mock('../../../../common/lib/kibana');
jest.mock('../hooks/use_assistant');
jest.mock(
'../../../../timelines/components/side_panel/event_details/use_get_alert_details_flyout_link'
);
moment.suppressDeprecationWarnings = true;
moment.tz.setDefault('UTC');
jest.mock('@elastic/eui', () => ({
...jest.requireActual('@elastic/eui'),
copyToClipboard: jest.fn(),
EuiCopy: jest.fn(({ children: functionAsChild }) => functionAsChild(jest.fn())),
}));
const alertUrl = 'https://example.com/alert';
const dateFormat = 'MMM D, YYYY @ HH:mm:ss.SSS';
const flyoutContextValue = {} as unknown as ExpandableFlyoutContext;
const mockContextValue = {
@ -55,7 +38,7 @@ const renderHeader = (contextValue: RightPanelContext) =>
<TestProvidersComponent>
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
<RightPanelContext.Provider value={contextValue}>
<HeaderTitle flyoutIsExpandable={true} />
<HeaderTitle />
</RightPanelContext.Provider>
</ExpandableFlyoutContext.Provider>
</TestProvidersComponent>
@ -65,8 +48,6 @@ describe('<HeaderTitle />', () => {
beforeEach(() => {
jest.mocked(useDateFormat).mockImplementation(() => dateFormat);
jest.mocked(useTimeZone).mockImplementation(() => 'UTC');
jest.mocked(useAssistant).mockReturnValue({ showAssistant: true, promptContextId: '' });
jest.mocked(useGetAlertDetailsFlyoutLink).mockReturnValue(alertUrl);
});
it('should render component', () => {
@ -74,7 +55,7 @@ describe('<HeaderTitle />', () => {
expect(getByTestId(FLYOUT_HEADER_TITLE_TEST_ID)).toBeInTheDocument();
expect(getByTestId(RISK_SCORE_VALUE_TEST_ID)).toBeInTheDocument();
expect(getByTestId(SEVERITY_TITLE_TEST_ID)).toBeInTheDocument();
expect(getByTestId(SEVERITY_VALUE_TEST_ID)).toBeInTheDocument();
});
it('should render rule name in the title if document is an alert', () => {
@ -83,50 +64,6 @@ describe('<HeaderTitle />', () => {
expect(getByTestId(FLYOUT_HEADER_TITLE_TEST_ID)).toHaveTextContent('rule-name');
});
it('should render share button in the title and copy the the value to clipboard', () => {
const syncedFlyoutState = 'flyoutState';
const query = `?${FLYOUT_URL_PARAM}=${syncedFlyoutState}`;
Object.defineProperty(window, 'location', {
value: {
search: query,
},
});
const { getByTestId } = renderHeader(mockContextValue);
const shareButton = getByTestId(SHARE_BUTTON_TEST_ID);
expect(shareButton).toBeInTheDocument();
fireEvent.click(shareButton);
expect(copyToClipboard).toHaveBeenCalledWith(
`${alertUrl}&${FLYOUT_URL_PARAM}=${syncedFlyoutState}`
);
});
it('should not render share button in the title if alert is missing url info', () => {
jest.mocked(useGetAlertDetailsFlyoutLink).mockReturnValue(null);
const { queryByTestId } = renderHeader(mockContextValue);
expect(queryByTestId(SHARE_BUTTON_TEST_ID)).not.toBeInTheDocument();
});
it('should render chat button in the title', () => {
const { getByTestId } = renderHeader(mockContextValue);
expect(getByTestId(CHAT_BUTTON_TEST_ID)).toBeInTheDocument();
});
it('should not render chat button in the title if should not be shown', () => {
jest.mocked(useAssistant).mockReturnValue({ showAssistant: false, promptContextId: '' });
const { queryByTestId } = renderHeader(mockContextValue);
expect(queryByTestId(CHAT_BUTTON_TEST_ID)).not.toBeInTheDocument();
});
it('should render default document detail title if document is not an alert', () => {
const contextValue = {
...mockContextValue,

View file

@ -5,133 +5,112 @@
* 2.0.
*/
import type { VFC } from 'react';
import React, { memo } from 'react';
import { NewChatById } from '@kbn/elastic-assistant';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui';
import { isEmpty } from 'lodash';
import { css } from '@emotion/react';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { FLYOUT_URL_PARAM } from '../../shared/hooks/url/use_sync_flyout_state_with_url';
import { CopyToClipboard } from '../../../shared/components/copy_to_clipboard';
import { useGetAlertDetailsFlyoutLink } from '../../../../timelines/components/side_panel/event_details/use_get_alert_details_flyout_link';
import { DocumentStatus } from './status';
import { useAssistant } from '../hooks/use_assistant';
import type { FC } from 'react';
import React, { memo, useMemo } from 'react';
import {
ALERT_SUMMARY_CONVERSATION_ID,
EVENT_SUMMARY_CONVERSATION_ID,
} from '../../../../common/components/event_details/translations';
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiTitle,
useEuiTheme,
EuiTextColor,
EuiIcon,
EuiToolTip,
} from '@elastic/eui';
import { isEmpty } from 'lodash';
import { FormattedMessage } from '@kbn/i18n-react';
import { css } from '@emotion/react';
import { DocumentStatus } from './status';
import { DocumentSeverity } from './severity';
import { RiskScore } from './risk_score';
import { useBasicDataFromDetailsData } from '../../../../timelines/components/side_panel/event_details/helpers';
import { useRightPanelContext } from '../context';
import { PreferenceFormattedDate } from '../../../../common/components/formatted_date';
import { FLYOUT_HEADER_TITLE_TEST_ID, SHARE_BUTTON_TEST_ID } from './test_ids';
export interface HeaderTitleProps {
/**
* If false, update the margin-top to compensate the fact that the expand detail button is not displayed
*/
flyoutIsExpandable: boolean;
}
import { RenderRuleName } from '../../../../timelines/components/timeline/body/renderers/formatted_field_helpers';
import { SIGNAL_RULE_NAME_FIELD_NAME } from '../../../../timelines/components/timeline/body/renderers/constants';
import { FLYOUT_HEADER_TITLE_TEST_ID } from './test_ids';
/**
* Document details flyout right section header
*/
export const HeaderTitle: VFC<HeaderTitleProps> = memo(({ flyoutIsExpandable }) => {
const { dataFormattedForFieldBrowser, eventId, indexName } = useRightPanelContext();
const { isAlert, ruleName, timestamp } = useBasicDataFromDetailsData(
export const HeaderTitle: FC = memo(() => {
const { dataFormattedForFieldBrowser, eventId, scopeId } = useRightPanelContext();
const { isAlert, ruleName, timestamp, ruleId } = useBasicDataFromDetailsData(
dataFormattedForFieldBrowser
);
const alertDetailsLink = useGetAlertDetailsFlyoutLink({
_id: eventId,
_index: indexName,
timestamp,
});
const { euiTheme } = useEuiTheme();
const ruleTitle = useMemo(
() => (
<EuiToolTip content={ruleName}>
<RenderRuleName
contextId={scopeId}
eventId={eventId}
fieldName={SIGNAL_RULE_NAME_FIELD_NAME}
fieldType={'string'}
isAggregatable={false}
isDraggable={false}
linkValue={ruleId}
value={ruleName}
openInNewTab
>
<div
css={css`
word-break: break-word;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
margin-right: ${euiTheme.size.base};
`}
>
<EuiIcon type={'warning'} size="m" className="eui-alignBaseline" />
&nbsp;
<EuiTitle size="s">
<EuiTextColor color={euiTheme.colors.primaryText}>
<span data-test-subj={FLYOUT_HEADER_TITLE_TEST_ID}>{ruleName}</span>
</EuiTextColor>
</EuiTitle>
&nbsp;
<EuiIcon
type={'popout'}
size="m"
css={css`
display: inline;
position: absolute;
bottom: ${euiTheme.size.xs};
right: 0;
`}
/>
</div>
</RenderRuleName>
</EuiToolTip>
),
[ruleName, ruleId, eventId, scopeId, euiTheme.colors.primaryText, euiTheme.size]
);
const showShareAlertButton = isAlert && alertDetailsLink;
const { showAssistant, promptContextId } = useAssistant({
dataFormattedForFieldBrowser,
isAlert,
});
const eventTitle = (
<EuiTitle size="s">
<h2 data-test-subj={FLYOUT_HEADER_TITLE_TEST_ID}>
<FormattedMessage
id="xpack.securitySolution.flyout.right.header.headerTitle"
defaultMessage="Event details"
/>
</h2>
</EuiTitle>
);
return (
<>
{(showShareAlertButton || showAssistant) && (
<EuiFlexGroup
direction="row"
justifyContent="flexEnd"
gutterSize="none"
css={css`
margin-top: ${flyoutIsExpandable ? '-44px' : '-28px'};
padding: 0 25px;
`}
>
{showAssistant && (
<EuiFlexItem grow={false}>
<NewChatById
conversationId={
isAlert ? ALERT_SUMMARY_CONVERSATION_ID : EVENT_SUMMARY_CONVERSATION_ID
}
promptContextId={promptContextId}
/>
</EuiFlexItem>
)}
{showShareAlertButton && (
<EuiFlexItem grow={false}>
<CopyToClipboard
rawValue={alertDetailsLink}
modifier={(value: string) => {
const query = new URLSearchParams(window.location.search);
return `${value}&${FLYOUT_URL_PARAM}=${query.get(FLYOUT_URL_PARAM)}`;
}}
text={
<FormattedMessage
id="xpack.securitySolution.flyout.right.header.shareButtonLabel"
defaultMessage="Share Alert"
/>
}
iconType={'share'}
ariaLabel={i18n.translate(
'xpack.securitySolution.flyout.right.header.shareButtonAriaLabel',
{
defaultMessage: 'Share Alert',
}
)}
data-test-subj={SHARE_BUTTON_TEST_ID}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
)}
<EuiSpacer size="s" />
<EuiTitle size="s">
<h2 data-test-subj={FLYOUT_HEADER_TITLE_TEST_ID}>
{isAlert && !isEmpty(ruleName) ? (
ruleName
) : (
<FormattedMessage
id="xpack.securitySolution.flyout.right.header.headerTitle"
defaultMessage="Event details"
/>
)}
</h2>
</EuiTitle>
<EuiSpacer size="s" />
<EuiFlexGroup direction="row" gutterSize={isAlert ? 'm' : 'none'}>
<EuiFlexItem grow={false}>
<DocumentStatus />
</EuiFlexItem>
<EuiFlexItem grow={false}>
{timestamp && <PreferenceFormattedDate value={new Date(timestamp)} />}
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
<DocumentSeverity />
<EuiSpacer size="m" />
{timestamp && <PreferenceFormattedDate value={new Date(timestamp)} />}
<EuiSpacer size="xs" />
{isAlert && !isEmpty(ruleName) ? ruleTitle : eventTitle}
<EuiSpacer size="m" />
<EuiFlexGroup direction="row" gutterSize="m">
<EuiFlexItem grow={false}>
<DocumentSeverity />
<DocumentStatus />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<RiskScore />

View file

@ -8,7 +8,7 @@
import React from 'react';
import { render } from '@testing-library/react';
import { RightPanelContext } from '../context';
import { SEVERITY_TITLE_TEST_ID, SEVERITY_VALUE_TEST_ID } from './test_ids';
import { SEVERITY_VALUE_TEST_ID } from './test_ids';
import { DocumentSeverity } from './severity';
import { mockGetFieldsData } from '../../shared/mocks/mock_get_fields_data';
import { TestProviders } from '../../../../common/mock';
@ -31,7 +31,6 @@ describe('<DocumentSeverity />', () => {
const { getByTestId } = renderDocumentSeverity(contextValue);
expect(getByTestId(SEVERITY_TITLE_TEST_ID)).toBeInTheDocument();
const severity = getByTestId(SEVERITY_VALUE_TEST_ID);
expect(severity).toBeInTheDocument();
expect(severity).toHaveTextContent('Low');

View file

@ -7,17 +7,14 @@
import type { FC } from 'react';
import React, { memo } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
import { ALERT_SEVERITY } from '@kbn/rule-data-utils';
import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types';
import { CellActionsMode } from '@kbn/cell-actions';
import { FormattedMessage } from '@kbn/i18n-react';
import { getSourcererScopeId } from '../../../../helpers';
import { SecurityCellActions } from '../../../../common/components/cell_actions';
import { SecurityCellActionsTrigger } from '../../../../actions/constants';
import { useRightPanelContext } from '../context';
import { SeverityBadge } from '../../../../detections/components/rules/severity_badge';
import { SEVERITY_TITLE_TEST_ID } from './test_ids';
const isSeverity = (x: unknown): x is Severity =>
x === 'low' || x === 'medium' || x === 'high' || x === 'critical';
@ -43,33 +40,19 @@ export const DocumentSeverity: FC = memo(() => {
}
return (
<EuiFlexGroup alignItems="center" direction="row" gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiTitle size="xxs" data-test-subj={SEVERITY_TITLE_TEST_ID}>
<h3>
<FormattedMessage
id="xpack.securitySolution.flyout.right.header.severityTitle"
defaultMessage="Severity:"
/>
</h3>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<SecurityCellActions
data={{
field: ALERT_SEVERITY,
value: alertSeverity,
}}
mode={CellActionsMode.HOVER_RIGHT}
triggerId={SecurityCellActionsTrigger.DETAILS_FLYOUT}
visibleCellActions={6}
sourcererScopeId={getSourcererScopeId(scopeId)}
metadata={{ scopeId }}
>
<SeverityBadge value={alertSeverity} />
</SecurityCellActions>
</EuiFlexItem>
</EuiFlexGroup>
<SecurityCellActions
data={{
field: ALERT_SEVERITY,
value: alertSeverity,
}}
mode={CellActionsMode.HOVER_RIGHT}
triggerId={SecurityCellActionsTrigger.DETAILS_FLYOUT}
visibleCellActions={6}
sourcererScopeId={getSourcererScopeId(scopeId)}
metadata={{ scopeId }}
>
<SeverityBadge value={alertSeverity} />
</SecurityCellActions>
);
});

View file

@ -12,11 +12,7 @@ import { CONTENT_TEST_ID, HEADER_TEST_ID } from './expandable_section';
const FLYOUT_HEADER_TEST_ID = `${PREFIX}Header` as const;
export const FLYOUT_HEADER_TITLE_TEST_ID = `${FLYOUT_HEADER_TEST_ID}Title` as const;
export const EXPAND_DETAILS_BUTTON_TEST_ID = `${FLYOUT_HEADER_TEST_ID}ExpandDetailButton` as const;
export const COLLAPSE_DETAILS_BUTTON_TEST_ID =
`${FLYOUT_HEADER_TEST_ID}CollapseDetailButton` as const;
export const STATUS_BUTTON_TEST_ID = 'rule-status-badge' as const;
export const SEVERITY_TITLE_TEST_ID = `${FLYOUT_HEADER_TEST_ID}SeverityTitle` as const;
export const SEVERITY_VALUE_TEST_ID = 'severity' as const;
export const RISK_SCORE_TITLE_TEST_ID = `${FLYOUT_HEADER_TEST_ID}RiskScoreTitle` as const;
export const RISK_SCORE_VALUE_TEST_ID = `${FLYOUT_HEADER_TEST_ID}RiskScoreValue` as const;

View file

@ -5,12 +5,12 @@
* 2.0.
*/
import { EuiFlyoutBody } from '@elastic/eui';
import type { VFC } from 'react';
import React, { useMemo } from 'react';
import { FLYOUT_BODY_TEST_ID } from './test_ids';
import type { RightPanelPaths } from '.';
import type { RightPanelTabsType } from './tabs';
import { FlyoutBody } from '../../shared/components/flyout_body';
import {} from './tabs';
export interface PanelContentProps {
@ -33,7 +33,7 @@ export const PanelContent: VFC<PanelContentProps> = ({ selectedTabId, tabs }) =>
return tabs.find((tab) => tab.id === selectedTabId)?.content;
}, [selectedTabId, tabs]);
return <EuiFlyoutBody data-test-subj={FLYOUT_BODY_TEST_ID}>{selectedTabContent}</EuiFlyoutBody>;
return <FlyoutBody data-test-subj={FLYOUT_BODY_TEST_ID}>{selectedTabContent}</FlyoutBody>;
};
PanelContent.displayName = 'PanelContent';

View file

@ -8,9 +8,12 @@
import type { FC } from 'react';
import React, { useCallback } from 'react';
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
import { EuiPanel } from '@elastic/eui';
import { FlyoutFooter } from '../../../timelines/components/side_panel/event_details/flyout';
import { useRightPanelContext } from './context';
import { useHostIsolationTools } from '../../../timelines/components/side_panel/event_details/use_host_isolation_tools';
import { DEFAULT_DARK_MODE } from '../../../../common/constants';
import { useUiSetting } from '../../../common/lib/kibana';
/**
*
@ -25,6 +28,7 @@ export const PanelFooter: FC = () => {
refetchFlyoutData,
scopeId,
} = useRightPanelContext();
const isDarkMode = useUiSetting<boolean>(DEFAULT_DARK_MODE);
const { isHostIsolationPanelOpen, showHostIsolationPanel } = useHostIsolationTools();
@ -45,16 +49,24 @@ export const PanelFooter: FC = () => {
);
return (
<FlyoutFooter
detailsData={dataFormattedForFieldBrowser}
detailsEcsData={dataAsNestedObject}
handleOnEventClosed={closeFlyout}
isHostIsolationPanelOpen={isHostIsolationPanelOpen}
isReadOnly={false}
loadingEventDetails={false}
onAddIsolationStatusClick={showHostIsolationPanelCallback}
scopeId={scopeId}
refetchFlyoutData={refetchFlyoutData}
/>
<EuiPanel
hasShadow={false}
borderRadius="none"
style={{
backgroundColor: isDarkMode ? `rgb(37, 38, 46)` : `rgb(241, 244, 250)`,
}}
>
<FlyoutFooter
detailsData={dataFormattedForFieldBrowser}
detailsEcsData={dataAsNestedObject}
handleOnEventClosed={closeFlyout}
isHostIsolationPanelOpen={isHostIsolationPanelOpen}
isReadOnly={false}
loadingEventDetails={false}
onAddIsolationStatusClick={showHostIsolationPanelCallback}
scopeId={scopeId}
refetchFlyoutData={refetchFlyoutData}
/>
</EuiPanel>
);
};

View file

@ -1,60 +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 React from 'react';
import { render } from '@testing-library/react';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { TestProviders } from '../../../common/mock';
import { RightPanelContext } from './context';
import { mockContextValue } from './mocks/mock_context';
import { PanelHeader } from './header';
import {
COLLAPSE_DETAILS_BUTTON_TEST_ID,
EXPAND_DETAILS_BUTTON_TEST_ID,
} from './components/test_ids';
import { mockFlyoutContextValue } from '../shared/mocks/mock_flyout_context';
describe('<PanelHeader />', () => {
it('should render expand details button if flyout is expandable', () => {
const { getByTestId } = render(
<TestProviders>
<ExpandableFlyoutContext.Provider value={mockFlyoutContextValue}>
<RightPanelContext.Provider value={mockContextValue}>
<PanelHeader
flyoutIsExpandable={true}
selectedTabId={'overview'}
setSelectedTabId={() => window.alert('test')}
tabs={[]}
/>
</RightPanelContext.Provider>
</ExpandableFlyoutContext.Provider>
</TestProviders>
);
expect(getByTestId(EXPAND_DETAILS_BUTTON_TEST_ID)).toBeInTheDocument();
});
it('should not render expand details button if flyout is not expandable', () => {
const { queryByTestId } = render(
<TestProviders>
<ExpandableFlyoutContext.Provider value={mockFlyoutContextValue}>
<RightPanelContext.Provider value={mockContextValue}>
<PanelHeader
flyoutIsExpandable={false}
selectedTabId={'overview'}
setSelectedTabId={() => window.alert('test')}
tabs={[]}
/>
</RightPanelContext.Provider>
</ExpandableFlyoutContext.Provider>
</TestProviders>
);
expect(queryByTestId(EXPAND_DETAILS_BUTTON_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(COLLAPSE_DETAILS_BUTTON_TEST_ID)).not.toBeInTheDocument();
});
});

View file

@ -5,14 +5,14 @@
* 2.0.
*/
import { EuiFlyoutHeader, EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui';
import type { VFC } from 'react';
import { EuiSpacer, EuiTab } from '@elastic/eui';
import type { FC } from 'react';
import React, { memo } from 'react';
import { css } from '@emotion/react';
import type { RightPanelPaths } from '.';
import type { RightPanelTabsType } from './tabs';
import { FlyoutHeader } from '../../shared/components/flyout_header';
import { FlyoutHeaderTabs } from '../../shared/components/flyout_header_tabs';
import { HeaderTitle } from './components/header_title';
import { ExpandDetailButton } from './components/expand_detail_button';
export interface PanelHeaderProps {
/**
@ -28,14 +28,10 @@ export interface PanelHeaderProps {
* Tabs to display in the header
*/
tabs: RightPanelTabsType;
/**
* If true, the expand detail button will be displayed
*/
flyoutIsExpandable: boolean;
}
export const PanelHeader: VFC<PanelHeaderProps> = memo(
({ flyoutIsExpandable, selectedTabId, setSelectedTabId, tabs }) => {
export const PanelHeader: FC<PanelHeaderProps> = memo(
({ selectedTabId, setSelectedTabId, tabs }) => {
const onSelectedTabChanged = (id: RightPanelPaths) => setSelectedTabId(id);
const renderTabs = tabs.map((tab, index) => (
<EuiTab
@ -49,31 +45,11 @@ export const PanelHeader: VFC<PanelHeaderProps> = memo(
));
return (
<EuiFlyoutHeader hasBorder>
{flyoutIsExpandable && (
<div
// moving the buttons up in the header
css={css`
margin-top: -24px;
margin-left: -8px;
`}
>
<ExpandDetailButton />
</div>
)}
<EuiSpacer size="xs" />
<HeaderTitle flyoutIsExpandable={flyoutIsExpandable} />
<FlyoutHeader>
<HeaderTitle />
<EuiSpacer size="m" />
<EuiTabs
size="l"
expand
css={css`
margin-bottom: -25px;
`}
>
{renderTabs}
</EuiTabs>
</EuiFlyoutHeader>
<FlyoutHeaderTabs>{renderTabs}</FlyoutHeaderTabs>
</FlyoutHeader>
);
}
);

View file

@ -12,6 +12,7 @@ import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
import { EventKind } from '../shared/constants/event_kinds';
import { getField } from '../shared/utils';
import { useRightPanelContext } from './context';
import { PanelNavigation } from './navigation';
import { PanelHeader } from './header';
import { PanelContent } from './content';
import type { RightPanelTabsType } from './tabs';
@ -64,8 +65,8 @@ export const RightPanel: FC<Partial<RightPanelProps>> = memo(({ path }) => {
return (
<>
<PanelNavigation flyoutIsExpandable={documentIsSignal} />
<PanelHeader
flyoutIsExpandable={documentIsSignal}
tabs={tabsDisplayed}
selectedTabId={selectedTabId}
setSelectedTabId={setSelectedTabId}

View file

@ -0,0 +1,47 @@
/*
* 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 type { FC } from 'react';
import React, { memo, useCallback } from 'react';
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
import { HeaderActions } from './components/header_actions';
import { FlyoutNavigation } from '../../shared/components/flyout_navigation';
import { LeftPanelKey } from '../left';
import { useRightPanelContext } from './context';
interface PanelNavigationProps {
/**
* If true, the expand detail button will be displayed
*/
flyoutIsExpandable: boolean;
}
export const PanelNavigation: FC<PanelNavigationProps> = memo(({ flyoutIsExpandable }) => {
const { openLeftPanel } = useExpandableFlyoutContext();
const { eventId, indexName, scopeId } = useRightPanelContext();
const expandDetails = useCallback(() => {
openLeftPanel({
id: LeftPanelKey,
params: {
id: eventId,
indexName,
scopeId,
},
});
}, [eventId, openLeftPanel, indexName, scopeId]);
return (
<FlyoutNavigation
flyoutIsExpandable={flyoutIsExpandable}
expandDetails={expandDetails}
actions={<HeaderActions />}
/>
);
});
PanelNavigation.displayName = 'PanelNavigation';

View file

@ -32,13 +32,13 @@ export const OverviewTab: FC = memo(() => {
)}
>
<AboutSection />
<EuiHorizontalRule margin="l" />
<EuiHorizontalRule margin="m" />
<InvestigationSection />
<EuiHorizontalRule margin="l" />
<EuiHorizontalRule margin="m" />
<VisualizationsSection />
<EuiHorizontalRule margin="l" />
<EuiHorizontalRule margin="m" />
<InsightsSection />
<EuiHorizontalRule margin="l" />
<EuiHorizontalRule margin="m" />
<ResponseSection />
</EuiPanel>
);

View file

@ -100,6 +100,7 @@ export const SecuritySolutionFlyout = memo(() => {
<ExpandableFlyout
registeredPanels={expandableFlyoutDocumentsPanels}
handleOnFlyoutClosed={handleFlyoutChangedOrClosed}
paddingSize="none"
/>
);
});

View file

@ -78,3 +78,48 @@ export const MultipleSizes: Story<void> = () => {
</EuiFlexGroup>
);
};
export const ButtonOnly: Story<void> = () => {
return (
<CopyToClipboard
rawValue={json}
modifier={(value) => {
window.alert('modifier');
return value;
}}
iconType={'copyClipboard'}
ariaLabel={'Copy'}
/>
);
};
export const CustomColor: Story<void> = () => {
return (
<CopyToClipboard
rawValue={json}
modifier={(value) => {
window.alert('modifier');
return value;
}}
iconType={'copyClipboard'}
ariaLabel={'Copy'}
text={<p>{'showing custom color'}</p>}
color={'accent'}
/>
);
};
export const CustomIcon: Story<void> = () => {
return (
<CopyToClipboard
rawValue={json}
modifier={(value) => {
window.alert('modifier');
return value;
}}
iconType={'share'}
ariaLabel={'Share'}
text={<p>{'custom icon'}</p>}
/>
);
};

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import type { EuiButtonEmptyProps } from '@elastic/eui';
import { copyToClipboard, EuiButtonEmpty, EuiCopy } from '@elastic/eui';
import type { FC, ReactElement } from 'react';
import React from 'react';
@ -21,15 +22,19 @@ export interface CopyToClipboardProps {
/**
* Button main text (next to icon)
*/
text: ReactElement;
text?: ReactElement;
/**
* Icon name (value coming from EUI)
*/
iconType: string;
iconType: EuiButtonEmptyProps['iconType'];
/**
* Button size (values coming from EUI)
*/
size?: 's' | 'm' | 'xs';
size?: EuiButtonEmptyProps['size'];
/**
* Optional button color
*/
color?: EuiButtonEmptyProps['color'];
/**
* Aria label value for the button
*/
@ -49,6 +54,7 @@ export const CopyToClipboard: FC<CopyToClipboardProps> = ({
text,
iconType,
size = 'm',
color = 'primary',
ariaLabel,
'data-test-subj': dataTestSubj,
}) => {
@ -68,6 +74,7 @@ export const CopyToClipboard: FC<CopyToClipboardProps> = ({
}}
iconType={iconType}
size={size}
color={color}
aria-label={ariaLabel}
data-test-subj={dataTestSubj}
>

View file

@ -0,0 +1,21 @@
/*
* 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 { render } from '@testing-library/react';
import { FlyoutBody } from './flyout_body';
const text = 'some text';
const dataTestSubj = 'flyout body';
describe('<FlyoutBody />', () => {
it('should render body', () => {
const { getByTestId } = render(<FlyoutBody data-test-subj={dataTestSubj}>{text}</FlyoutBody>);
expect(getByTestId(dataTestSubj)).toBeInTheDocument();
expect(getByTestId(dataTestSubj)).toHaveTextContent(text);
});
});

View file

@ -0,0 +1,29 @@
/*
* 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 type { FC } from 'react';
import React, { memo } from 'react';
import { EuiFlyoutBody, EuiPanel } from '@elastic/eui';
interface FlyoutBodyProps extends React.ComponentProps<typeof EuiFlyoutBody> {
children: React.ReactNode;
}
/**
* Wrapper of `EuiFlyoutBody`, setting the recommended `16px` padding using a EuiPanel.
*/
export const FlyoutBody: FC<FlyoutBodyProps> = memo(({ children, ...flyoutBodyProps }) => {
return (
<EuiFlyoutBody {...flyoutBodyProps}>
<EuiPanel hasShadow={false} color="transparent">
{children}
</EuiPanel>
</EuiFlyoutBody>
);
});
FlyoutBody.displayName = 'FlyoutBody';

View file

@ -0,0 +1,23 @@
/*
* 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 { render } from '@testing-library/react';
import { FlyoutFooter } from './flyout_footer';
const text = 'some text';
const dataTestSubj = 'flyout footer';
describe('<FlyoutFooter />', () => {
it('should render footer', () => {
const { getByTestId } = render(
<FlyoutFooter data-test-subj={dataTestSubj}>{text}</FlyoutFooter>
);
expect(getByTestId(dataTestSubj)).toBeInTheDocument();
expect(getByTestId(dataTestSubj)).toHaveTextContent(text);
});
});

View file

@ -0,0 +1,29 @@
/*
* 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 type { FC } from 'react';
import React, { memo } from 'react';
import { EuiFlyoutFooter, EuiPanel } from '@elastic/eui';
interface FlyoutFooterProps extends React.ComponentProps<typeof EuiFlyoutFooter> {
children: React.ReactNode;
}
/**
* Wrapper of `EuiFlyoutFooter`, setting the recommended `16px` padding using a EuiPanel.
*/
export const FlyoutFooter: FC<FlyoutFooterProps> = memo(({ children, ...flyoutFooterProps }) => {
return (
<EuiFlyoutFooter {...flyoutFooterProps}>
<EuiPanel hasShadow={false} color="transparent">
{children}
</EuiPanel>
</EuiFlyoutFooter>
);
});
FlyoutFooter.displayName = 'FlyoutFooter';

View file

@ -0,0 +1,23 @@
/*
* 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 { render } from '@testing-library/react';
import { FlyoutHeader } from './flyout_header';
const text = 'some text';
const dataTestSubj = 'flyout header';
describe('<FlyoutHeader />', () => {
it('should render header', () => {
const { getByTestId } = render(
<FlyoutHeader data-test-subj={dataTestSubj}>{text}</FlyoutHeader>
);
expect(getByTestId(dataTestSubj)).toBeInTheDocument();
expect(getByTestId(dataTestSubj)).toHaveTextContent(text);
});
});

View file

@ -0,0 +1,29 @@
/*
* 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 type { FC } from 'react';
import React, { memo } from 'react';
import { EuiFlyoutHeader, EuiPanel } from '@elastic/eui';
interface FlyoutHeaderProps extends React.ComponentProps<typeof EuiFlyoutHeader> {
children: React.ReactNode;
}
/**
* Wrapper of `EuiFlyoutHeader`, setting the recommended `16px` padding using a EuiPanel.
*/
export const FlyoutHeader: FC<FlyoutHeaderProps> = memo(({ children, ...flyoutHeaderProps }) => {
return (
<EuiFlyoutHeader hasBorder {...flyoutHeaderProps}>
<EuiPanel hasShadow={false} color="transparent">
{children}
</EuiPanel>
</EuiFlyoutHeader>
);
});
FlyoutHeader.displayName = 'FlyoutHeader';

View file

@ -0,0 +1,26 @@
/*
* 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 { render } from '@testing-library/react';
import { EuiTab } from '@elastic/eui';
import { FlyoutHeaderTabs } from './flyout_header_tabs';
const tab = 'tab name';
const dataTestSubj = 'flyout tabs';
describe('<FlyoutTabs />', () => {
it('should render tabs', () => {
const { getByTestId } = render(
<FlyoutHeaderTabs data-test-subj={dataTestSubj}>
{[<EuiTab key={1}>{tab}</EuiTab>]}
</FlyoutHeaderTabs>
);
expect(getByTestId(dataTestSubj)).toBeInTheDocument();
expect(getByTestId(dataTestSubj)).toHaveTextContent(tab);
});
});

View file

@ -0,0 +1,37 @@
/*
* 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 type { FC } from 'react';
import React, { memo } from 'react';
import { EuiTabs } from '@elastic/eui';
import { css } from '@emotion/react';
interface FlyoutHeaderTabsProps extends React.ComponentProps<typeof EuiTabs> {
children: React.ReactNode;
}
/**
* Wrapper of `EuiTabs`, setting bottom margin to align with the flyout header divider
*/
export const FlyoutHeaderTabs: FC<FlyoutHeaderTabsProps> = memo(
({ children, ...flyoutTabsProps }) => {
return (
<EuiTabs
size="l"
expand
css={css`
margin-bottom: -17px;
`}
{...flyoutTabsProps}
>
{children}
</EuiTabs>
);
}
);
FlyoutHeaderTabs.displayName = 'FlyoutHeaderTabs';

View file

@ -0,0 +1,74 @@
/*
* 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 type { Story } from '@storybook/react';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { EuiButtonIcon } from '@elastic/eui';
import { FlyoutNavigation } from './flyout_navigation';
const expandDetails = () => window.alert('expand left panel');
export default {
component: FlyoutNavigation,
title: 'Flyout/Navigation',
};
const flyoutContextValue = {
closeLeftPanel: () => window.alert('close left panel'),
panels: {},
} as unknown as ExpandableFlyoutContext;
export const Expand: Story<void> = () => {
return (
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
<FlyoutNavigation flyoutIsExpandable={true} expandDetails={expandDetails} />
</ExpandableFlyoutContext.Provider>
);
};
export const Collapse: Story<void> = () => {
return (
<ExpandableFlyoutContext.Provider
value={
{
...flyoutContextValue,
panels: { left: {} },
} as unknown as ExpandableFlyoutContext
}
>
<FlyoutNavigation flyoutIsExpandable={true} expandDetails={expandDetails} />
</ExpandableFlyoutContext.Provider>
);
};
export const CollapsableWithAction: Story<void> = () => {
return (
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
<FlyoutNavigation
flyoutIsExpandable={true}
expandDetails={expandDetails}
actions={<EuiButtonIcon iconType="share" />}
/>
</ExpandableFlyoutContext.Provider>
);
};
export const NonCollapsableWithAction: Story<void> = () => {
return (
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
<FlyoutNavigation flyoutIsExpandable={false} actions={<EuiButtonIcon iconType="share" />} />
</ExpandableFlyoutContext.Provider>
);
};
export const Empty: Story<void> = () => {
return (
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
<FlyoutNavigation flyoutIsExpandable={false} />
</ExpandableFlyoutContext.Provider>
);
};

View file

@ -0,0 +1,110 @@
/*
* 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 { act, render } from '@testing-library/react';
import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context';
import { TestProviders } from '../../../common/mock';
import { FlyoutNavigation } from './flyout_navigation';
import {
COLLAPSE_DETAILS_BUTTON_TEST_ID,
EXPAND_DETAILS_BUTTON_TEST_ID,
HEADER_ACTIONS_TEST_ID,
} from './test_ids';
import { mockFlyoutContextValue } from '../../document_details/shared/mocks/mock_flyout_context';
const expandDetails = jest.fn();
describe('<FlyoutNavigation />', () => {
describe('when flyout is expandable', () => {
it('should render expand button', () => {
const flyoutContextValue = {
panels: {},
} as unknown as ExpandableFlyoutContext;
const { getByTestId, queryByTestId } = render(
<TestProviders>
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
<FlyoutNavigation flyoutIsExpandable={true} expandDetails={expandDetails} />
</ExpandableFlyoutContext.Provider>
</TestProviders>
);
expect(getByTestId(EXPAND_DETAILS_BUTTON_TEST_ID)).toBeInTheDocument();
expect(getByTestId(EXPAND_DETAILS_BUTTON_TEST_ID)).toHaveTextContent('Expand details');
expect(queryByTestId(COLLAPSE_DETAILS_BUTTON_TEST_ID)).not.toBeInTheDocument();
getByTestId(EXPAND_DETAILS_BUTTON_TEST_ID).click();
expect(expandDetails).toHaveBeenCalled();
});
it('should render collapse button', () => {
const flyoutContextValue = {
closeLeftPanel: jest.fn(),
panels: {
left: {},
},
} as unknown as ExpandableFlyoutContext;
const { getByTestId, queryByTestId } = render(
<TestProviders>
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
<FlyoutNavigation flyoutIsExpandable={true} expandDetails={expandDetails} />
</ExpandableFlyoutContext.Provider>
</TestProviders>
);
expect(getByTestId(COLLAPSE_DETAILS_BUTTON_TEST_ID)).toBeInTheDocument();
expect(getByTestId(COLLAPSE_DETAILS_BUTTON_TEST_ID)).toHaveTextContent('Collapse details');
expect(queryByTestId(EXPAND_DETAILS_BUTTON_TEST_ID)).not.toBeInTheDocument();
getByTestId(COLLAPSE_DETAILS_BUTTON_TEST_ID).click();
expect(flyoutContextValue.closeLeftPanel).toHaveBeenCalled();
});
});
it('should not render expand details button if flyout is not expandable', () => {
const { queryByTestId, getByTestId } = render(
<TestProviders>
<ExpandableFlyoutContext.Provider value={mockFlyoutContextValue}>
<FlyoutNavigation flyoutIsExpandable={false} actions={<div />} />
</ExpandableFlyoutContext.Provider>
</TestProviders>
);
expect(getByTestId(HEADER_ACTIONS_TEST_ID)).toBeInTheDocument();
expect(queryByTestId(EXPAND_DETAILS_BUTTON_TEST_ID)).not.toBeInTheDocument();
expect(queryByTestId(COLLAPSE_DETAILS_BUTTON_TEST_ID)).not.toBeInTheDocument();
});
it('should render actions if there are actions available', () => {
const { getByTestId } = render(
<TestProviders>
<ExpandableFlyoutContext.Provider value={mockFlyoutContextValue}>
<FlyoutNavigation
flyoutIsExpandable={true}
expandDetails={expandDetails}
actions={<div />}
/>
</ExpandableFlyoutContext.Provider>
</TestProviders>
);
expect(getByTestId(EXPAND_DETAILS_BUTTON_TEST_ID)).toBeInTheDocument();
expect(getByTestId(HEADER_ACTIONS_TEST_ID)).toBeInTheDocument();
});
it('should render empty component if panel is not expandable and no action is available', async () => {
const { container } = render(
<TestProviders>
<ExpandableFlyoutContext.Provider value={mockFlyoutContextValue}>
<FlyoutNavigation flyoutIsExpandable={false} />
</ExpandableFlyoutContext.Provider>
</TestProviders>
);
await act(async () => {
expect(container).toBeEmptyDOMElement();
});
});
});

View file

@ -0,0 +1,126 @@
/*
* 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 type { FC } from 'react';
import React, { memo, useCallback, useMemo } from 'react';
import {
EuiFlyoutHeader,
EuiFlexGroup,
EuiFlexItem,
useEuiTheme,
EuiButtonEmpty,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import {
HEADER_ACTIONS_TEST_ID,
COLLAPSE_DETAILS_BUTTON_TEST_ID,
EXPAND_DETAILS_BUTTON_TEST_ID,
} from './test_ids';
export interface PanelNavigationProps {
/**
* If true, the expand detail button will be displayed
*/
flyoutIsExpandable: boolean;
/**
* If flyoutIsExpandable is true, pass a callback to open left panel
*/
expandDetails?: () => void;
/**
* Optional actions to be placed on the right hand side of navigation
*/
actions?: React.ReactElement;
}
export const FlyoutNavigation: FC<PanelNavigationProps> = memo(
({ flyoutIsExpandable = false, expandDetails, actions }) => {
const { euiTheme } = useEuiTheme();
const { closeLeftPanel, panels } = useExpandableFlyoutContext();
const isExpanded: boolean = panels.left != null;
const collapseDetails = useCallback(() => closeLeftPanel(), [closeLeftPanel]);
const collapseButton = useMemo(
() => (
<EuiButtonEmpty
iconSide="left"
onClick={collapseDetails}
iconType="arrowEnd"
size="s"
data-test-subj={COLLAPSE_DETAILS_BUTTON_TEST_ID}
aria-label={i18n.translate(
'xpack.securitySolution.flyout.right.header.collapseDetailButtonAriaLabel',
{
defaultMessage: 'Collapse details',
}
)}
>
<FormattedMessage
id="xpack.securitySolution.flyout.right.header.collapseDetailButtonLabel"
defaultMessage="Collapse details"
/>
</EuiButtonEmpty>
),
[collapseDetails]
);
const expandButton = useMemo(
() => (
<EuiButtonEmpty
iconSide="left"
onClick={expandDetails}
iconType="arrowStart"
size="s"
data-test-subj={EXPAND_DETAILS_BUTTON_TEST_ID}
aria-label={i18n.translate(
'xpack.securitySolution.flyout.right.header.expandDetailButtonAriaLabel',
{
defaultMessage: 'Expand details',
}
)}
>
<FormattedMessage
id="xpack.securitySolution.flyout.right.header.expandDetailButtonLabel"
defaultMessage="Expand details"
/>
</EuiButtonEmpty>
),
[expandDetails]
);
return flyoutIsExpandable || actions ? (
<EuiFlyoutHeader hasBorder>
<EuiFlexGroup
direction="row"
justifyContent="spaceBetween"
alignItems="center"
gutterSize="none"
responsive={false}
css={css`
padding-left: ${euiTheme.size.s};
padding-right: ${euiTheme.size.l};
height: ${euiTheme.size.xxl};
`}
>
<EuiFlexItem grow={false}>
{flyoutIsExpandable && expandDetails && (isExpanded ? collapseButton : expandButton)}
</EuiFlexItem>
{actions && (
<EuiFlexItem grow={false} data-test-subj={HEADER_ACTIONS_TEST_ID}>
{actions}
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlyoutHeader>
) : null;
}
);
FlyoutNavigation.displayName = 'FlyoutNavigation';

View file

@ -23,3 +23,12 @@ export const EXPANDABLE_PANEL_HEADER_RIGHT_SECTION_TEST_ID = (dataTestSubj: stri
`${dataTestSubj}RightSection`;
export const EXPANDABLE_PANEL_LOADING_TEST_ID = (dataTestSubj: string) => `${dataTestSubj}Loading`;
export const EXPANDABLE_PANEL_CONTENT_TEST_ID = (dataTestSubj: string) => `${dataTestSubj}Content`;
/* Header Navigation */
const FLYOUT_NAVIGATION_TEST_ID = `${PREFIX}Navigation` as const;
export const EXPAND_DETAILS_BUTTON_TEST_ID =
`${FLYOUT_NAVIGATION_TEST_ID}ExpandDetailButton` as const;
export const COLLAPSE_DETAILS_BUTTON_TEST_ID =
`${FLYOUT_NAVIGATION_TEST_ID}CollapseDetailButton` as const;
export const HEADER_ACTIONS_TEST_ID = `${FLYOUT_NAVIGATION_TEST_ID}Actions` as const;

View file

@ -131,6 +131,7 @@ export const RenderRuleName: React.FC<RenderRuleNameProps> = ({
href={href}
data-test-subj="goToRuleDetails"
target="_blank"
external={false}
>
{children ?? content}
</LinkAnchor>

View file

@ -39,10 +39,8 @@ import {
DOCUMENT_DETAILS_FLYOUT_FOOTER_MARK_AS_CLOSED,
DOCUMENT_DETAILS_FLYOUT_FOOTER_RESPOND,
DOCUMENT_DETAILS_FLYOUT_FOOTER_TAKE_ACTION_BUTTON,
DOCUMENT_DETAILS_FLYOUT_HEADER_CHAT_BUTTON,
DOCUMENT_DETAILS_FLYOUT_HEADER_RISK_SCORE,
DOCUMENT_DETAILS_FLYOUT_HEADER_RISK_SCORE_VALUE,
DOCUMENT_DETAILS_FLYOUT_HEADER_SEVERITY,
DOCUMENT_DETAILS_FLYOUT_HEADER_SEVERITY_VALUE,
DOCUMENT_DETAILS_FLYOUT_HEADER_STATUS,
DOCUMENT_DETAILS_FLYOUT_HEADER_TITLE,
@ -84,8 +82,6 @@ describe.skip('Alert details expandable flyout right panel', () => {
cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_TITLE).should('be.visible').and('have.text', rule.name);
cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_CHAT_BUTTON).should('be.visible');
cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_STATUS).should('be.visible');
cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_RISK_SCORE).should('be.visible');
@ -93,7 +89,6 @@ describe.skip('Alert details expandable flyout right panel', () => {
.should('be.visible')
.and('have.text', rule.risk_score);
cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_SEVERITY).should('be.visible');
cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_SEVERITY_VALUE)
.should('be.visible')
.and('have.text', upperFirst(rule.severity));

View file

@ -12,16 +12,16 @@ import {
TABLE_TAB_TEST_ID,
} from '@kbn/security-solution-plugin/public/flyout/document_details/right/test_ids';
import {
COLLAPSE_DETAILS_BUTTON_TEST_ID,
EXPAND_DETAILS_BUTTON_TEST_ID,
CHAT_BUTTON_TEST_ID,
RISK_SCORE_TITLE_TEST_ID,
RISK_SCORE_VALUE_TEST_ID,
SEVERITY_TITLE_TEST_ID,
SEVERITY_VALUE_TEST_ID,
STATUS_BUTTON_TEST_ID,
FLYOUT_HEADER_TITLE_TEST_ID,
} from '@kbn/security-solution-plugin/public/flyout/document_details/right/components/test_ids';
import {
COLLAPSE_DETAILS_BUTTON_TEST_ID,
EXPAND_DETAILS_BUTTON_TEST_ID,
} from '@kbn/security-solution-plugin/public/flyout/shared/components/test_ids';
import { getDataTestSubjectSelector } from '../../helpers/common';
export const DOCUMENT_DETAILS_FLYOUT_BODY = getDataTestSubjectSelector(FLYOUT_BODY_TEST_ID);
@ -49,12 +49,8 @@ export const DOCUMENT_DETAILS_FLYOUT_HEADER_RISK_SCORE =
getDataTestSubjectSelector(RISK_SCORE_TITLE_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_HEADER_RISK_SCORE_VALUE =
getDataTestSubjectSelector(RISK_SCORE_VALUE_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_HEADER_SEVERITY =
getDataTestSubjectSelector(SEVERITY_TITLE_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_HEADER_SEVERITY_VALUE =
getDataTestSubjectSelector(SEVERITY_VALUE_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_HEADER_CHAT_BUTTON =
getDataTestSubjectSelector(CHAT_BUTTON_TEST_ID);
/* Footer */