[8.8] [Defend Workflows][Bug] Case flyout z-index (#153219) (#156505)

# Backport

This will backport the following commits from `main` to `8.8`:
- [[Defend Workflows][Bug] Case flyout z-index
(#153219)](https://github.com/elastic/kibana/pull/153219)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Konrad
Szwarc","email":"konrad.szwarc@elastic.co"},"sourceCommit":{"committedDate":"2023-05-03T10:30:13Z","message":"[Defend
Workflows][Bug] Case flyout z-index (#153219)\n\nFixes
https://github.com/elastic/security-team/issues/6228\r\n\r\n5000
`z-index` set in `create-case-flyout-mask-overlay` is
being\r\noverwritten by `euiOverlayMask-belowHeader` with a value of
1000. This\r\ncauses **Case flyout** to be under the already opened
**Osquery flyout**\r\n\r\nThis PR includes cleanup in flyouts renders -
we removed unnecessary\r\n`maskProps` as well as z-indexes that were
explicitly set even though\r\nflyout component manages them
itself.\r\n\r\n\r\n![test](https://user-images.githubusercontent.com/29123534/225292177-a08d3fb8-aad3-487b-a054-6cde6aec94d7.gif)\r\n\r\n---------\r\n\r\nCo-authored-by:
Tomasz Ciecierski <ciecierskitomek@gmail.com>\r\nCo-authored-by: Tomasz
Ciecierski <tomasz.ciecierski@elastic.co>\r\nCo-authored-by: Patryk
Kopyciński <contact@patrykkopycinski.com>\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"d8fe39c18d9df7acb00a82515f5a41a6e4111c59","branchLabelMapping":{"^v8.9.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:skip","Team:Defend
Workflows","Feature:Osquery","ci:all-cypress-suites","v8.8.0","ci:skip-cypress-osquery","v8.9.0"],"number":153219,"url":"https://github.com/elastic/kibana/pull/153219","mergeCommit":{"message":"[Defend
Workflows][Bug] Case flyout z-index (#153219)\n\nFixes
https://github.com/elastic/security-team/issues/6228\r\n\r\n5000
`z-index` set in `create-case-flyout-mask-overlay` is
being\r\noverwritten by `euiOverlayMask-belowHeader` with a value of
1000. This\r\ncauses **Case flyout** to be under the already opened
**Osquery flyout**\r\n\r\nThis PR includes cleanup in flyouts renders -
we removed unnecessary\r\n`maskProps` as well as z-indexes that were
explicitly set even though\r\nflyout component manages them
itself.\r\n\r\n\r\n![test](https://user-images.githubusercontent.com/29123534/225292177-a08d3fb8-aad3-487b-a054-6cde6aec94d7.gif)\r\n\r\n---------\r\n\r\nCo-authored-by:
Tomasz Ciecierski <ciecierskitomek@gmail.com>\r\nCo-authored-by: Tomasz
Ciecierski <tomasz.ciecierski@elastic.co>\r\nCo-authored-by: Patryk
Kopyciński <contact@patrykkopycinski.com>\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"d8fe39c18d9df7acb00a82515f5a41a6e4111c59"}},"sourceBranch":"main","suggestedTargetBranches":["8.8"],"targetPullRequestStates":[{"branch":"8.8","label":"v8.8.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.9.0","labelRegex":"^v8.9.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/153219","number":153219,"mergeCommit":{"message":"[Defend
Workflows][Bug] Case flyout z-index (#153219)\n\nFixes
https://github.com/elastic/security-team/issues/6228\r\n\r\n5000
`z-index` set in `create-case-flyout-mask-overlay` is
being\r\noverwritten by `euiOverlayMask-belowHeader` with a value of
1000. This\r\ncauses **Case flyout** to be under the already opened
**Osquery flyout**\r\n\r\nThis PR includes cleanup in flyouts renders -
we removed unnecessary\r\n`maskProps` as well as z-indexes that were
explicitly set even though\r\nflyout component manages them
itself.\r\n\r\n\r\n![test](https://user-images.githubusercontent.com/29123534/225292177-a08d3fb8-aad3-487b-a054-6cde6aec94d7.gif)\r\n\r\n---------\r\n\r\nCo-authored-by:
Tomasz Ciecierski <ciecierskitomek@gmail.com>\r\nCo-authored-by: Tomasz
Ciecierski <tomasz.ciecierski@elastic.co>\r\nCo-authored-by: Patryk
Kopyciński <contact@patrykkopycinski.com>\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"d8fe39c18d9df7acb00a82515f5a41a6e4111c59"}}]}]
BACKPORT-->

Co-authored-by: Konrad Szwarc <konrad.szwarc@elastic.co>
This commit is contained in:
Kibana Machine 2023-05-03 11:31:46 -04:00 committed by GitHub
parent df0d0976c2
commit b12b81230d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 245 additions and 239 deletions

View file

@ -96,7 +96,6 @@ export const SolutionSideNavPanel: React.FC<SolutionSideNavPanelProps> = React.m
return (
<>
{/* <GlobalPanelStyle /> */}
<EuiWindowEvent event="keydown" handler={onKeyDown} />
<EuiPortal>
<EuiFocusTrap autoFocus>

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import styled, { createGlobalStyle } from 'styled-components';
import styled from 'styled-components';
import { EuiFlyout, EuiFlyoutHeader, EuiTitle, EuiFlyoutBody } from '@elastic/eui';
import { QueryClientProvider } from '@tanstack/react-query';
@ -38,22 +38,6 @@ const StyledFlyout = styled(EuiFlyout)`
`}
`;
const maskOverlayClassName = 'create-case-flyout-mask-overlay';
/**
* We need to target the mask overlay which is a parent element
* of the flyout.
* A global style is needed to target a parent element.
*/
const GlobalStyle = createGlobalStyle<{ theme: { eui: { euiZLevel5: number } } }>`
.${maskOverlayClassName} {
${({ theme }) => `
z-index: ${theme.eui.euiZLevel5};
`}
}
`;
// Adding bottom padding because timeline's
// bottom bar gonna hide the submit button.
const StyledEuiFlyoutBody = styled(EuiFlyoutBody)`
@ -83,13 +67,10 @@ export const CreateCaseFlyout = React.memo<CreateCaseFlyoutProps>(
return (
<QueryClientProvider client={casesQueryClient}>
<ReactQueryDevtools initialIsOpen={false} />
<GlobalStyle />
<StyledFlyout
onClose={onClose}
tour-step="create-case-flyout"
data-test-subj="create-case-flyout"
// maskProps is needed in order to apply the z-index to the parent overlay element, not to the flyout only
maskProps={{ className: maskOverlayClassName }}
>
<EuiFlyoutHeader data-test-subj="create-case-flyout-header" hasBorder>
<EuiTitle size="m">

View file

@ -36,7 +36,12 @@ import {
viewRecentCaseAndCheckResults,
} from '../../tasks/live_query';
import { preparePack } from '../../tasks/packs';
import { closeModalIfVisible, closeToastIfVisible } from '../../tasks/integrations';
import {
closeModalIfVisible,
closeToastIfVisible,
generateRandomStringName,
interceptCaseId,
} from '../../tasks/integrations';
import { navigateTo } from '../../tasks/navigation';
import { RESULTS_TABLE, RESULTS_TABLE_BUTTON } from '../../screens/live_query';
import { OSQUERY_POLICY } from '../../screens/fleet';
@ -359,6 +364,59 @@ describe('Alert Event Details', () => {
});
});
describe('Case creation', () => {
let ruleId: string;
let ruleName: string;
let packId: string;
let packName: string;
let caseId: string;
const packData = packFixture();
before(() => {
loadPack(packData).then((data) => {
packId = data.id;
packName = data.attributes.name;
});
loadRule(true).then((data) => {
ruleId = data.id;
ruleName = data.name;
});
interceptCaseId((id) => {
caseId = id;
});
});
after(() => {
cleanupPack(packId);
cleanupRule(ruleId);
cleanupCase(caseId);
});
it('runs osquery against alert and creates a new case', () => {
const [caseName, caseDescription] = generateRandomStringName(2);
loadRuleAlerts(ruleName);
cy.getBySel('expand-event').first().click({ force: true });
cy.getBySel('take-action-dropdown-btn').click();
cy.getBySel('osquery-action-item').click();
cy.contains('Run a set of queries in a pack').wait(500).click();
cy.getBySel('select-live-pack').within(() => {
cy.getBySel('comboBoxInput').type(`${packName}{downArrow}{enter}`);
});
submitQuery();
cy.get('[aria-label="Add to Case"]').first().click();
cy.getBySel('cases-table-add-case-filter-bar').click();
cy.getBySel('create-case-flyout').should('be.visible');
cy.getBySel('caseTitle').within(() => {
cy.getBySel('input').type(caseName);
});
cy.getBySel('caseDescription').within(() => {
cy.getBySel('euiMarkdownEditorTextArea').type(caseDescription);
});
cy.getBySel('create-case-submit').click();
cy.contains(`An alert was added to "${caseName}"`);
});
});
describe('Case', () => {
let ruleId: string;
let ruleName: string;
@ -541,7 +599,7 @@ describe('Alert Event Details', () => {
});
});
cy.contains(timelineRegex);
cy.contains('Untitled timeline').click();
cy.getBySel('flyoutBottomBar').contains('Untitled timeline').click();
cy.contains(filterRegex);
});
});

View file

@ -60,6 +60,16 @@ export const interceptAgentPolicyId = (cb: (policyId: string) => void) => {
});
};
export const interceptCaseId = (cb: (caseId: string) => void) => {
cy.intercept('POST', '**/api/cases', (req) => {
req.continue((res) => {
cb(res.body.id);
return res.send(res.body);
});
});
};
export const interceptPackId = (cb: (packId: string) => void) => {
cy.intercept('POST', '**/api/osquery/packs', (req) => {
req.continue((res) => {

View file

@ -35,6 +35,8 @@ export const GLOBAL_SEARCH_BAR_FILTER_ITEM_DELETE = '#popoverFor_filter0 button[
export const GLOBAL_SEARCH_BAR_PINNED_FILTER = '.globalFilterItem-isPinned';
export const GLOBAL_KQL_WRAPPER = '[data-test-subj="filters-global-container"]';
export const GLOBAL_KQL_INPUT =
'[data-test-subj="filters-global-container"] [data-test-subj="unifiedQueryInput"] textarea';

View file

@ -209,7 +209,7 @@ export const TIMELINE_FILTER_OPERATOR = '[data-test-subj="filterOperatorList"]';
export const TIMELINE_FILTER_VALUE =
'[data-test-subj="filterParamsComboBox phraseParamsComboxBox"]';
export const TIMELINE_FLYOUT = '[data-test-subj="eui-flyout"]';
export const TIMELINE_FLYOUT = '[data-test-subj="timeline-flyout"]';
export const TIMELINE_FLYOUT_HEADER = '[data-test-subj="query-tab-flyout-header"]';

View file

@ -38,6 +38,8 @@ export const fillKqlQueryBar = (query: string) => {
export const clearKqlQueryBar = () => {
cy.get(GLOBAL_KQL_INPUT).should('be.visible');
cy.get(GLOBAL_KQL_INPUT).clear();
// clicks outside of the input to close the autocomplete
cy.get('body').click(0, 0);
};
export const removeKqlFilter = () => {

View file

@ -320,7 +320,7 @@ export const createNewTimelineTemplate = () => {
};
export const executeTimelineKQL = (query: string) => {
cy.get(`${SEARCH_OR_FILTER_CONTAINER} textarea`).type(`${query} {enter}`);
cy.get(`${SEARCH_OR_FILTER_CONTAINER} textarea`).clear().type(`${query} {enter}`);
};
export const executeTimelineSearch = (query: string) => {

View file

@ -35,41 +35,6 @@ export const FULL_SCREEN_CONTENT_OVERRIDES_CSS_STYLESHEET = () => css`
}
`;
/** The `z-index` for EuiPopover Panels that are displayed from inside of Timeline page */
export const TIMELINE_EUI_POPOVER_PANEL_ZINDEX = 9900;
/**
* Stylesheet with Eui class overrides in order to address display issues caused when
* the Timeline overlay is opened. These are normally adjustments to ensure that the
* z-index of other EUI components continues to work with the z-index used by timeline
* overlay.
*/
export const TIMELINE_OVERRIDES_CSS_STYLESHEET = () => css`
.euiPopover__panel[data-popover-open] {
z-index: ${TIMELINE_EUI_POPOVER_PANEL_ZINDEX} !important;
min-width: 24px;
}
.euiPopover__panel[data-popover-open].sourcererPopoverPanel {
// needs to appear under modal
z-index: 5900 !important;
}
.euiToolTip {
z-index: 9950 !important;
}
/*
overrides the default styling of euiComboBoxOptionsList because it's implemented
as a popover, so it's not selectable as a child of the styled component
*/
.euiComboBoxOptionsList {
z-index: 9999;
}
/* ensure elastic charts tooltips appear above open euiPopovers */
.echTooltip {
z-index: 9950;
}
`;
/*
SIDE EFFECT: the following `createGlobalStyle` overrides default styling in angular code that was not theme-friendly
and `EuiPopover`, `EuiToolTip` global styles
@ -77,20 +42,15 @@ export const TIMELINE_OVERRIDES_CSS_STYLESHEET = () => css`
export const AppGlobalStyle = createGlobalStyle<{
theme: { eui: { euiColorPrimary: string; euiColorLightShade: string; euiSizeS: string } };
}>`
${TIMELINE_OVERRIDES_CSS_STYLESHEET}
/*
overrides the default styling of EuiDataGrid expand popover footer to
make it a column of actions instead of the default actions row
*/
.euiDataGridRowCell__popover {
max-width: 815px !important;
max-height: none !important;
overflow: hidden;
.expandable-top-value-button {
&.euiButtonEmpty--primary:enabled:focus,
.euiButtonEmpty--primary:focus {
@ -111,8 +71,8 @@ export const AppGlobalStyle = createGlobalStyle<{
}
}
.euiText + .euiPopoverFooter {
border-top: 1px solid ${({ theme }) => theme.eui.euiColorLightShade};
.euiText + .euiPopoverFooter {
border-top: 1px solid ${({ theme }) => theme.eui.euiColorLightShade};
margin-top: ${({ theme }) => theme.eui.euiSizeS};
}
}

View file

@ -37,7 +37,6 @@ const MyEuiModal = styled(EuiModal)`
height: auto !important;
max-width: 718px;
}
z-index: 99999999;
`;
export const UpdateDefaultDataViewModal = React.memo<Props>(

View file

@ -26,14 +26,12 @@ const TopNContainer = styled.div`
`;
const CloseButton = styled(EuiButtonIcon)`
z-index: 999999;
position: absolute;
right: 4px;
top: 4px;
`;
const ViewSelect = styled(EuiSuperSelect)`
z-index: 999999;
width: 170px;
`;

View file

@ -80,7 +80,6 @@ export interface AddExceptionFlyoutProps {
sharedListToAddTo?: ExceptionListSchema[];
onCancel: (didRuleChange: boolean) => void;
onConfirm: (didRuleChange: boolean, didCloseAlert: boolean, didBulkCloseAlert: boolean) => void;
isNonTimeline?: boolean;
}
const FlyoutBodySection = styled(EuiFlyoutBody)`
@ -114,7 +113,6 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({
sharedListToAddTo,
onCancel,
onConfirm,
isNonTimeline = false,
}: AddExceptionFlyoutProps) {
const { isLoading, indexPatterns } = useFetchIndexPatterns(rules);
const [isSubmitting, submitNewExceptionItems] = useAddNewExceptionItems();
@ -462,13 +460,7 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({
}, [listType]);
return (
<EuiFlyout
ownFocus
maskProps={{ style: isNonTimeline === false ? 'z-index: 5000' : 'z-index: 1000' }} // For an edge case to display above the timeline flyout
size="l"
onClose={handleCloseFlyout}
data-test-subj="addExceptionFlyout"
>
<EuiFlyout size="l" onClose={handleCloseFlyout} data-test-subj="addExceptionFlyout">
<FlyoutHeader>
<EuiTitle>
<h2 data-test-subj="exceptionFlyoutTitle">{addExceptionMessage}</h2>

View file

@ -499,7 +499,6 @@ const ExceptionsViewerComponent = ({
onConfirm={handleConfirmExceptionFlyout}
data-test-subj="addExceptionItemFlyout"
showAlertCloseOptions
isNonTimeline={true}
/>
)}

View file

@ -24,7 +24,7 @@ import type {
ExceptionsBuilderReturnExceptionItem,
} from '@kbn/securitysolution-list-utils';
import type { DataViewBase } from '@kbn/es-query';
import styled, { css, createGlobalStyle } from 'styled-components';
import styled, { css } from 'styled-components';
import { ENDPOINT_LIST_ID } from '@kbn/securitysolution-list-constants';
import { hasEqlSequenceQuery, isEqlRule } from '../../../../../../common/detection_engine/utils';
import type { Rule } from '../../../../rule_management/logic/types';
@ -56,15 +56,6 @@ const SectionHeader = styled(EuiTitle)`
font-weight: ${({ theme }) => theme.eui.euiFontWeightSemiBold};
`}
`;
// EuiCombox doesn't support change of z-index, or providing any class to portal
// This fix ovveride z-index for EuiFlyout, which conflict with EuiComboBox on this flyout
// fix x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.tsx#L429
// TODO: should be fixed on Component level
const EuiComboboxZIndexGlobalStyle = createGlobalStyle`
[data-test-subj="comboBoxOptionsList osSelectionDropdown-optionsList"] {
z-index: 6000 !important;
}
`;
interface ExceptionsFlyoutConditionsComponentProps {
/* Exception list item field value for "name" */
@ -242,7 +233,6 @@ const ExceptionsConditionsComponent: React.FC<ExceptionsFlyoutConditionsComponen
data-test-subj="osSelectionDropdown"
/>
</EuiFormRow>
<EuiComboboxZIndexGlobalStyle />
<EuiSpacer size="l" />
</>
)}

View file

@ -52,12 +52,7 @@ const OsqueryFlyoutComponent: React.FC<OsqueryFlyoutProps> = ({
if (osquery?.OsqueryAction) {
return (
<EuiFlyout
ownFocus
maskProps={{ style: 'z-index: 5000' }} // For an edge case to display above the timeline flyout
size="m"
onClose={onClose}
>
<EuiFlyout size="m" onClose={onClose}>
<EuiFlyoutHeader hasBorder data-test-subj="flyout-header-osquery">
<EuiTitle>
<h2>{ACTION_OSQUERY}</h2>

View file

@ -247,7 +247,6 @@ export const ExceptionsListCard = memo<ExceptionsListCardProps>(
onConfirm={handleConfirmExceptionFlyout}
data-test-subj="addExceptionItemFlyoutInSharedLists"
showAlertCloseOptions={false}
isNonTimeline={true}
/>
) : null}
{showEditExceptionFlyout && exceptionToEdit ? (

View file

@ -69,7 +69,6 @@ const ListWithSearchComponent: FC<ListWithSearchComponentProps> = ({
onConfirm={handleConfirmExceptionFlyout}
data-test-subj="addExceptionItemFlyoutInList"
showAlertCloseOptions={false} // TODO ask if we need it
isNonTimeline={true}
// ask if we need the add to rule/list section and which list should we link the exception here
/>
) : viewerStatus === ViewerStatus.EMPTY || viewerStatus === ViewerStatus.LOADING ? (

View file

@ -539,7 +539,6 @@ export const SharedLists = React.memo(() => {
setDisplayAddExceptionItemFlyout(false);
if (didRuleChange) handleRefresh();
}}
isNonTimeline={true}
/>
)}

View file

@ -15,15 +15,7 @@ import type { EuiPortalProps } from '@elastic/eui/src/components/portal/portal';
import type { EuiTheme } from '@kbn/kibana-react-plugin/common';
import { useIsMounted } from '@kbn/securitysolution-hook-utils';
import { useHasFullScreenContent } from '../../../common/containers/use_full_screen';
import {
FULL_SCREEN_CONTENT_OVERRIDES_CSS_STYLESHEET,
TIMELINE_EUI_POPOVER_PANEL_ZINDEX,
TIMELINE_OVERRIDES_CSS_STYLESHEET,
} from '../../../common/components/page';
import {
SELECTOR_TIMELINE_IS_VISIBLE_CSS_CLASS_NAME,
TIMELINE_EUI_THEME_ZINDEX_LEVEL,
} from '../../../timelines/components/timeline/styles';
import { FULL_SCREEN_CONTENT_OVERRIDES_CSS_STYLESHEET } from '../../../common/components/page';
const OverlayRootContainer = styled.div`
border: none;
@ -88,18 +80,6 @@ const PageOverlayGlobalStyles = createGlobalStyle<{ theme: EuiTheme }>`
overflow: hidden;
}
//-------------------------------------------------------------------------------------------
// Style overrides for when Page Overlay is shown over SecuritySolutionPageWrapper component
//-------------------------------------------------------------------------------------------
// That page wrapper includes several global EUI styles that can conflict with content shown
// from inside of this Page Overlay component.
//-------------------------------------------------------------------------------------------
// Eui Confirm Dialog mask overlay should be displayed above any other popovers
//-------------------------------------------------------------------------------------------
body.${PAGE_OVERLAY_DOCUMENT_BODY_OVER_PAGE_WRAPPER_CLASSNAME} .euiOverlayMask[data-relative-to-header="above"] {
z-index: ${TIMELINE_EUI_POPOVER_PANEL_ZINDEX};
}
//-------------------------------------------------------------------------------------------
// Style overrides for when Page Overlay is in full screen mode
//-------------------------------------------------------------------------------------------
@ -109,32 +89,6 @@ const PageOverlayGlobalStyles = createGlobalStyle<{ theme: EuiTheme }>`
body.${PAGE_OVERLAY_DOCUMENT_BODY_FULLSCREEN_CLASSNAME} {
${FULL_SCREEN_CONTENT_OVERRIDES_CSS_STYLESHEET}
}
//-------------------------------------------------------------------------------------------
// TIMELINE SPECIFIC STYLES
//-------------------------------------------------------------------------------------------
// The timeline overlay uses a custom z-index, which causes issues with any other content that
// is normally appended to the 'document.body' directly (like popups, masks, flyouts, etc).
// The styles below will be applied anytime the timeline is opened/visible and attempts to
// mitigate the issues around z-index so that content that is shown after the PageOverlay is
// opened is displayed properly.
//-------------------------------------------------------------------------------------------
body.${SELECTOR_TIMELINE_IS_VISIBLE_CSS_CLASS_NAME}.${PAGE_OVERLAY_DOCUMENT_BODY_IS_VISIBLE_CLASSNAME} {
.${PAGE_OVERLAY_CSS_CLASSNAME},
.euiOverlayMask,
.euiFlyout {
z-index: ${({ theme: { eui } }) => eui[TIMELINE_EUI_THEME_ZINDEX_LEVEL]};
}
// Confirm Dialog mask overlay should be displayed above any other popover
.euiOverlayMask[data-relative-to-header="above"] {
z-index: ${TIMELINE_EUI_POPOVER_PANEL_ZINDEX};
}
// Other Timeline overrides from AppGlobalStyle:
// x-pack/plugins/security_solution/public/common/components/page/index.tsx
${TIMELINE_OVERRIDES_CSS_STYLESHEET}
}
`;
const setDocumentBodyOverlayIsVisible = () => {

View file

@ -23,6 +23,7 @@ import {
import { lastValueFrom } from 'rxjs';
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
import type { EuiOverlayMaskProps } from '@elastic/eui/src/components/overlay_mask';
import { useWithArtifactSubmitData } from '../../../../components/artifact_list_page/hooks/use_with_artifact_submit_data';
import type {
ArtifactFormComponentOnChangeCallbackProps,
@ -40,9 +41,7 @@ import { getCreationSuccessMessage, getCreationErrorMessage } from '../translati
export interface EventFiltersFlyoutProps {
data?: Ecs;
onCancel(): void;
maskProps?: {
style?: string;
};
maskProps?: EuiOverlayMaskProps;
}
export const EventFiltersFlyout: React.FC<EventFiltersFlyoutProps> = memo(

View file

@ -7,16 +7,7 @@ exports[`Flyout rendering it renders correctly against snapshot 1`] = `
>
<div
data-test-subj="flyout-pane"
style="display: none;"
>
<div
data-eui="EuiFlyout"
data-test-subj="eui-flyout"
role="dialog"
>
<div />
</div>
</div>
/>
</div>
.c2 {
display: block;

View file

@ -5,16 +5,19 @@
* 2.0.
*/
import { EuiFocusTrap, EuiOutsideClickDetector } from '@elastic/eui';
import { EuiFocusTrap, EuiOutsideClickDetector, EuiWindowEvent, keys } from '@elastic/eui';
import React, { useEffect, useMemo, useCallback, useState, useRef } from 'react';
import type { AppLeaveHandler } from '@kbn/core/public';
import { useDispatch } from 'react-redux';
import type { TimelineId } from '../../../../common/types/timeline';
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
import { FlyoutBottomBar } from './bottom_bar';
import { Pane } from './pane';
import { getTimelineShowStatusByIdSelector } from './selectors';
import { useTimelineSavePrompt } from '../../../common/hooks/timeline/use_timeline_save_prompt';
import { timelineActions } from '../../store/timeline';
import { focusActiveTimelineButton } from '../timeline/helpers';
interface OwnProps {
timelineId: TimelineId;
@ -26,6 +29,12 @@ type VoidFunc = () => void;
const FlyoutComponent: React.FC<OwnProps> = ({ timelineId, onAppLeave }) => {
const getTimelineShowStatus = useMemo(() => getTimelineShowStatusByIdSelector(), []);
const { show } = useDeepEqualSelector((state) => getTimelineShowStatus(state, timelineId));
const dispatch = useDispatch();
const handleClose = useCallback(() => {
dispatch(timelineActions.showTimeline({ id: timelineId, show: false }));
focusActiveTimelineButton();
}, [dispatch, timelineId]);
const [focusOwnership, setFocusOwnership] = useState(true);
const [triggerOnBlur, setTriggerOnBlur] = useState(true);
@ -37,6 +46,7 @@ const FlyoutComponent: React.FC<OwnProps> = ({ timelineId, onAppLeave }) => {
setFocusOwnership(true);
}
}, [show, focusOwnership]);
const onOutsideClick = useCallback((event) => {
setFocusOwnership(false);
const classes = event.target.classList;
@ -51,6 +61,16 @@ const FlyoutComponent: React.FC<OwnProps> = ({ timelineId, onAppLeave }) => {
}
}, []);
// ESC key closes Pane
const onKeyDown = useCallback(
(ev: KeyboardEvent) => {
if (ev.key === keys.ESCAPE) {
handleClose();
}
},
[handleClose]
);
useTimelineSavePrompt(timelineId, onAppLeave);
useEffect(() => {
@ -75,6 +95,7 @@ const FlyoutComponent: React.FC<OwnProps> = ({ timelineId, onAppLeave }) => {
<Pane timelineId={timelineId} visible={show} />
</EuiFocusTrap>
<FlyoutBottomBar showTimelineHeaderPanel={!show} timelineId={timelineId} />
<EuiWindowEvent event="keydown" handler={onKeyDown} />
</>
</EuiOutsideClickDetector>
);

View file

@ -0,0 +1,96 @@
/*
* 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.
*/
/**
* NOTE: We can't test this component because Enzyme doesn't support rendering
* into portals.
*/
import deepEqual from 'fast-deep-equal';
import type { ReactNode } from 'react';
import { Component } from 'react';
import { createPortal } from 'react-dom';
interface InsertPositionsMap {
after: InsertPosition;
before: InsertPosition;
}
export const insertPositions: InsertPositionsMap = {
after: 'afterend',
before: 'beforebegin',
};
export interface EuiPortalProps {
/**
* ReactNode to render as this component's content
*/
children: ReactNode;
insert?: { sibling: HTMLElement | null; position: 'before' | 'after' };
portalRef?: (ref: HTMLDivElement | null) => void;
}
export class EuiPortal extends Component<EuiPortalProps> {
portalNode: HTMLDivElement | null = null;
constructor(props: EuiPortalProps) {
super(props);
if (typeof window === 'undefined') return; // Prevent SSR errors
const { insert } = this.props;
this.portalNode = document.createElement('div');
this.portalNode.dataset.euiportal = 'true';
if (insert == null || insert.sibling == null) {
// no insertion defined, append to body
document.body.appendChild(this.portalNode);
} else {
// inserting before or after an element
const { sibling, position } = insert;
sibling.insertAdjacentElement(insertPositions[position], this.portalNode);
}
}
componentDidMount() {
this.updatePortalRef(this.portalNode);
}
componentWillUnmount() {
if (this.portalNode?.parentNode) {
this.portalNode.parentNode.removeChild(this.portalNode);
}
this.updatePortalRef(null);
}
componentDidUpdate(prevProps: Readonly<EuiPortalProps>): void {
if (!deepEqual(prevProps.insert, this.props.insert) && this.portalNode?.parentNode) {
this.portalNode.parentNode.removeChild(this.portalNode);
}
if (this.portalNode) {
if (this.props.insert == null || this.props.insert.sibling == null) {
// no insertion defined, append to body
document.body.appendChild(this.portalNode);
} else {
// inserting before or after an element
const { sibling, position } = this.props.insert;
sibling.insertAdjacentElement(insertPositions[position], this.portalNode);
}
}
}
updatePortalRef(ref: HTMLDivElement | null) {
if (this.props.portalRef) {
this.props.portalRef(ref);
}
}
render() {
return this.portalNode ? createPortal(this.props.children, this.portalNode) : null;
}
}

View file

@ -50,14 +50,14 @@ describe('Pane', () => {
});
});
test('renders with display none when visibility is set to false', async () => {
test.skip('renders with display none when visibility is set to false', async () => {
const EmptyComponent = render(
<TestProviders>
<Pane timelineId={TimelineId.test} visible={false} />
</TestProviders>
);
await waitFor(() => {
expect(EmptyComponent.getByTestId('flyout-pane')).toHaveStyle('display: none');
expect(EmptyComponent.getByTestId('timeline-flyout')).toHaveStyle('display: none');
});
});
});

View file

@ -5,83 +5,60 @@
* 2.0.
*/
import type { EuiFlyoutProps } from '@elastic/eui';
import { EuiFlyout } from '@elastic/eui';
import React, { useCallback, useEffect } from 'react';
import styled, { createGlobalStyle } from 'styled-components';
import { useDispatch } from 'react-redux';
import React, { useMemo, useRef } from 'react';
import { css } from '@emotion/react';
import { useEuiBackgroundColor, useEuiTheme } from '@elastic/eui';
import {
SELECTOR_TIMELINE_IS_VISIBLE_CSS_CLASS_NAME,
TIMELINE_EUI_THEME_ZINDEX_LEVEL,
} from '../../timeline/styles';
import { StatefulTimeline } from '../../timeline';
import type { TimelineId } from '../../../../../common/types/timeline';
import * as i18n from './translations';
import { timelineActions } from '../../../store/timeline';
import { defaultRowRenderers } from '../../timeline/body/renderers';
import { DefaultCellRenderer } from '../../timeline/cell_rendering/default_cell_renderer';
import { focusActiveTimelineButton } from '../../timeline/helpers';
import { EuiPortal } from './custom_portal';
interface FlyoutPaneComponentProps {
timelineId: TimelineId;
visible?: boolean;
}
const StyledEuiFlyout = styled(EuiFlyout)<EuiFlyoutProps>`
animation: none;
min-width: 150px;
z-index: ${({ theme }) => theme.eui[TIMELINE_EUI_THEME_ZINDEX_LEVEL]};
`;
// SIDE EFFECT: the following creates a global class selector
const IndexPatternFieldEditorOverlayGlobalStyle = createGlobalStyle<{
theme: { eui: { euiZLevel5: number } };
}>`
.euiOverlayMask.indexPatternFieldEditorMaskOverlay {
${({ theme }) => `
z-index: ${theme.eui.euiZLevel5};
`}
}
`;
const FlyoutPaneComponent: React.FC<FlyoutPaneComponentProps> = ({
timelineId,
visible = true,
}) => {
const dispatch = useDispatch();
const handleClose = useCallback(() => {
dispatch(timelineActions.showTimeline({ id: timelineId, show: false }));
focusActiveTimelineButton();
}, [dispatch, timelineId]);
const { euiTheme } = useEuiTheme();
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (visible) {
document.body.classList.add(SELECTOR_TIMELINE_IS_VISIBLE_CSS_CLASS_NAME);
} else {
document.body.classList.remove(SELECTOR_TIMELINE_IS_VISIBLE_CSS_CLASS_NAME);
}
}, [visible]);
const timeline = useMemo(
() => (
<StatefulTimeline
renderCellValue={DefaultCellRenderer}
rowRenderers={defaultRowRenderers}
timelineId={timelineId}
/>
),
[timelineId]
);
return (
<div data-test-subj="flyout-pane" style={{ display: visible ? 'block' : 'none' }}>
<StyledEuiFlyout
aria-label={i18n.TIMELINE_DESCRIPTION}
className="timeline-flyout"
data-test-subj="eui-flyout"
hideCloseButton={true}
onClose={handleClose}
size="100%"
ownFocus={false}
style={{ display: visible ? 'block' : 'none' }}
>
<IndexPatternFieldEditorOverlayGlobalStyle />
<StatefulTimeline
renderCellValue={DefaultCellRenderer}
rowRenderers={defaultRowRenderers}
timelineId={timelineId}
/>
</StyledEuiFlyout>
<div data-test-subj="flyout-pane" ref={ref}>
<EuiPortal insert={{ sibling: !visible ? ref?.current : null, position: 'after' }}>
<div
aria-label={i18n.TIMELINE_DESCRIPTION}
className="euiFlyout"
data-test-subj="timeline-flyout"
css={css`
min-width: 150px;
height: calc(100% - 96px);
top: 96px;
background: ${useEuiBackgroundColor('plain')};
position: fixed;
width: 100%;
z-index: ${euiTheme.levels.flyout};
display: ${visible ? 'block' : 'none'};
`}
>
{timeline}
</div>
</EuiPortal>
</div>
);
};

View file

@ -187,11 +187,7 @@ export const FlyoutFooterComponent = React.memo(
/>
)}
{isAddEventFilterModalOpen && detailsEcsData != null && (
<EventFiltersFlyout
data={detailsEcsData}
onCancel={closeAddEventFilterModal}
maskProps={{ style: 'z-index: 5000' }}
/>
<EventFiltersFlyout data={detailsEcsData} onCancel={closeAddEventFilterModal} />
)}
{isOsqueryFlyoutOpenWithAgentId && detailsEcsData != null && (
<OsqueryFlyout

View file

@ -15,16 +15,6 @@ import type { TimelineEventsType } from '../../../../common/types/timeline';
import { ACTIONS_COLUMN_ARIA_COL_INDEX } from './helpers';
import { EVENTS_TABLE_ARIA_LABEL } from './translations';
/**
* The EUI theme's z-index property that is used by the timeline overlay
*/
export const TIMELINE_EUI_THEME_ZINDEX_LEVEL = 'euiZLevel4';
/**
* The css classname added to the `document.body` whenever the timeline is visible on the page
*/
export const SELECTOR_TIMELINE_IS_VISIBLE_CSS_CLASS_NAME = 'securitySolutionTimeline-isVisible';
/**
* TIMELINE BODY
*/

View file

@ -9,7 +9,7 @@ import { subj as testSubjSelector } from '@kbn/test-subj-selector';
import { DATE_RANGE_OPTION_TO_TEST_SUBJ_MAP } from '@kbn/security-solution-plugin/common/test';
import { FtrService } from '../../../functional/ftr_provider_context';
const TIMELINE_BOTTOM_BAR_CONTAINER_TEST_SUBJ = 'timeline-bottom-bar-container';
const TIMELINE_BOTTOM_BAR_CONTAINER_TEST_SUBJ = 'flyoutBottomBar';
const TIMELINE_CLOSE_BUTTON_TEST_SUBJ = 'close-timeline';
const TIMELINE_MODAL_PAGE_TEST_SUBJ = 'timeline';
const TIMELINE_TAB_QUERY_TEST_SUBJ = 'timeline-tab-content-query';