mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution] Expandable flyout - right panel header refactor (#170279)
This commit is contained in:
parent
5e8414779a
commit
3bf58b04ab
42 changed files with 975 additions and 575 deletions
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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,
|
||||
|
|
|
@ -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" />
|
||||
|
||||
<EuiTitle size="s">
|
||||
<EuiTextColor color={euiTheme.colors.primaryText}>
|
||||
<span data-test-subj={FLYOUT_HEADER_TITLE_TEST_ID}>{ruleName}</span>
|
||||
</EuiTextColor>
|
||||
</EuiTitle>
|
||||
|
||||
<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 />
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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';
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -100,6 +100,7 @@ export const SecuritySolutionFlyout = memo(() => {
|
|||
<ExpandableFlyout
|
||||
registeredPanels={expandableFlyoutDocumentsPanels}
|
||||
handleOnFlyoutClosed={handleFlyoutChangedOrClosed}
|
||||
paddingSize="none"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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>}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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;
|
||||
|
|
|
@ -131,6 +131,7 @@ export const RenderRuleName: React.FC<RenderRuleNameProps> = ({
|
|||
href={href}
|
||||
data-test-subj="goToRuleDetails"
|
||||
target="_blank"
|
||||
external={false}
|
||||
>
|
||||
{children ?? content}
|
||||
</LinkAnchor>
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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 */
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue