[RAC][Security Solution] Make analyzer work with EuiDataGrid full screen (#110913)

* Make analyzer work with EuiDataGrid full screen

* Don't ever restrict the width, remove console.log

* Remove isEventViewer prop no longer used

* Make global filters appear below data grid
This commit is contained in:
Kevin Qualters 2021-09-02 13:21:44 -04:00 committed by GitHub
parent 0452483e7d
commit f8e86f5e02
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 202 additions and 125 deletions

View file

@ -11,7 +11,7 @@ import { useGlobalHeaderPortal } from '../../../../common/hooks/use_global_heade
const StyledStickyWrapper = styled.div`
position: sticky;
z-index: ${(props) => props.theme.eui.euiZLevel2};
z-index: ${(props) => props.theme.eui.euiZHeaderBelowDataGrid};
// TOP location is declared in src/public/rendering/_base.scss to keep in line with Kibana Chrome
`;

View file

@ -321,7 +321,7 @@ const EventsViewerComponent: React.FC<Props> = ({
refetch={refetch}
/>
{graphEventId && <GraphOverlay isEventViewer={true} timelineId={id} />}
{graphEventId && <GraphOverlay timelineId={id} />}
<FullWidthFlexGroup $visible={!graphEventId} gutterSize="none">
<ScrollableFlexItem grow={1}>
<StatefulBody

View file

@ -147,9 +147,7 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
const trailingControlColumns: ControlColumnProps[] = EMPTY_CONTROL_COLUMNS;
const graphOverlay = useMemo(
() =>
graphEventId != null && graphEventId.length > 0 ? (
<GraphOverlay isEventViewer={true} timelineId={id} />
) : null,
graphEventId != null && graphEventId.length > 0 ? <GraphOverlay timelineId={id} /> : null,
[graphEventId, id]
);
const setQuery = useCallback(

View file

@ -28,7 +28,6 @@ export const resetScroll = () => {
}
}, 0);
};
interface GlobalFullScreen {
globalFullScreen: boolean;
setGlobalFullScreen: (fullScreen: boolean) => void;
@ -46,10 +45,10 @@ export const useGlobalFullScreen = (): GlobalFullScreen => {
const setGlobalFullScreen = useCallback(
(fullScreen: boolean) => {
if (fullScreen) {
document.body.classList.add(SCROLLING_DISABLED_CLASS_NAME);
document.body.classList.add(SCROLLING_DISABLED_CLASS_NAME, 'euiDataGrid__restrictBody');
resetScroll();
} else {
document.body.classList.remove(SCROLLING_DISABLED_CLASS_NAME);
document.body.classList.remove(SCROLLING_DISABLED_CLASS_NAME, 'euiDataGrid__restrictBody');
resetScroll();
}
@ -71,9 +70,15 @@ export const useTimelineFullScreen = (): TimelineFullScreen => {
const dispatch = useDispatch();
const timelineFullScreen =
useShallowEqualSelector(inputsSelectors.timelineFullScreenSelector) ?? false;
const setTimelineFullScreen = useCallback(
(fullScreen: boolean) => dispatch(inputsActions.setFullScreen({ id: 'timeline', fullScreen })),
(fullScreen: boolean) => {
if (fullScreen) {
document.body.classList.add('euiDataGrid__restrictBody');
} else {
document.body.classList.remove('euiDataGrid__restrictBody');
}
dispatch(inputsActions.setFullScreen({ id: 'timeline', fullScreen }));
},
[dispatch]
);
const memoizedReturn = useMemo(

View file

@ -13,6 +13,10 @@ import {
setActiveTabTimeline,
updateTimelineGraphEventId,
} from '../../../../timelines/store/timeline/actions';
import {
useGlobalFullScreen,
useTimelineFullScreen,
} from '../../../../common/containers/use_full_screen';
import { TimelineId, TimelineTabs } from '../../../../../common';
import { ACTION_INVESTIGATE_IN_RESOLVER } from '../../../../timelines/components/timeline/body/translations';
import { Ecs } from '../../../../../common/ecs';
@ -35,13 +39,23 @@ export const useInvestigateInResolverContextItem = ({
}: InvestigateInResolverProps) => {
const dispatch = useDispatch();
const isDisabled = useMemo(() => !isInvestigateInResolverActionEnabled(ecsData), [ecsData]);
const { setGlobalFullScreen } = useGlobalFullScreen();
const { setTimelineFullScreen } = useTimelineFullScreen();
const handleClick = useCallback(() => {
const dataGridIsFullScreen = document.querySelector('.euiDataGrid--fullScreen');
dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: ecsData._id }));
if (timelineId === TimelineId.active) {
if (dataGridIsFullScreen) {
setTimelineFullScreen(true);
}
dispatch(setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.graph }));
} else {
if (dataGridIsFullScreen) {
setGlobalFullScreen(true);
}
}
onClose();
}, [dispatch, ecsData._id, onClose, timelineId]);
}, [dispatch, ecsData._id, onClose, timelineId, setGlobalFullScreen, setTimelineFullScreen]);
return isDisabled
? []
: [

View file

@ -44,12 +44,10 @@ describe('GraphOverlay', () => {
});
describe('when used in an events viewer (i.e. in the Detections view, or the Host > Events view)', () => {
const isEventViewer = true;
test('it has 100% width when isEventViewer is true and NOT in full screen mode', async () => {
test('it has 100% width when NOT in full screen mode', async () => {
const wrapper = mount(
<TestProviders>
<GraphOverlay timelineId={TimelineId.test} isEventViewer={isEventViewer} />
<GraphOverlay timelineId={TimelineId.test} />
</TestProviders>
);
@ -59,9 +57,9 @@ describe('GraphOverlay', () => {
});
});
test('it has a calculated width that makes room for the Timeline flyout button when isEventViewer is true in full screen mode', async () => {
test('it has a fixed position when in full screen mode', async () => {
(useGlobalFullScreen as jest.Mock).mockReturnValue({
globalFullScreen: true, // <-- true when an events viewer is in full screen mode
globalFullScreen: true,
setGlobalFullScreen: jest.fn(),
});
(useTimelineFullScreen as jest.Mock).mockReturnValue({
@ -71,25 +69,24 @@ describe('GraphOverlay', () => {
const wrapper = mount(
<TestProviders>
<GraphOverlay timelineId={TimelineId.test} isEventViewer={isEventViewer} />
<GraphOverlay timelineId={TimelineId.test} />
</TestProviders>
);
await waitFor(() => {
const overlayContainer = wrapper.find('[data-test-subj="overlayContainer"]').first();
expect(overlayContainer).toHaveStyleRule('width', 'calc(100% - 36px)');
expect(overlayContainer).toHaveStyleRule('position', 'fixed');
});
});
});
describe('when used in the active timeline', () => {
const isEventViewer = false;
const timelineId = TimelineId.active;
test('it has 100% width when isEventViewer is false and NOT in full screen mode', async () => {
test('it has 100% width when NOT in full screen mode', async () => {
const wrapper = mount(
<TestProviders>
<GraphOverlay timelineId={timelineId} isEventViewer={isEventViewer} />
<GraphOverlay timelineId={timelineId} />
</TestProviders>
);
@ -99,7 +96,7 @@ describe('GraphOverlay', () => {
});
});
test('it has 100% width when isEventViewer is false and the active timeline is in full screen mode', async () => {
test('it has 100% width when the active timeline is in full screen mode', async () => {
(useGlobalFullScreen as jest.Mock).mockReturnValue({
globalFullScreen: false,
setGlobalFullScreen: jest.fn(),
@ -111,7 +108,7 @@ describe('GraphOverlay', () => {
const wrapper = mount(
<TestProviders>
<GraphOverlay timelineId={timelineId} isEventViewer={isEventViewer} />
<GraphOverlay timelineId={timelineId} />
</TestProviders>
);

View file

@ -41,13 +41,19 @@ import {
import * as i18n from './translations';
const OverlayContainer = styled.div`
${({ $restrictWidth }: { $restrictWidth: boolean }) =>
`
display: flex;
flex-direction: column;
flex: 1;
width: ${$restrictWidth ? 'calc(100% - 36px)' : '100%'};
`}
display: flex;
flex-direction: column;
flex: 1;
width: 100%;
`;
const FullScreenOverlayContainer = styled.div`
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: ${(props) => props.theme.eui.euiZLevel3};
`;
const StyledResolver = styled(Resolver)`
@ -59,7 +65,6 @@ const FullScreenButtonIcon = styled(EuiButtonIcon)`
`;
interface OwnProps {
isEventViewer: boolean;
timelineId: TimelineId;
}
@ -111,18 +116,15 @@ NavigationComponent.displayName = 'NavigationComponent';
const Navigation = React.memo(NavigationComponent);
const GraphOverlayComponent: React.FC<OwnProps> = ({ isEventViewer, timelineId }) => {
const GraphOverlayComponent: React.FC<OwnProps> = ({ timelineId }) => {
const dispatch = useDispatch();
const onCloseOverlay = useCallback(() => {
dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: '' }));
}, [dispatch, timelineId]);
const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen();
const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen();
const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []);
const graphEventId = useDeepEqualSelector(
(state) => (getTimeline(state, timelineId) ?? timelineDefaults).graphEventId
);
const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen();
const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen();
const getStartSelector = useMemo(() => startSelector(), []);
const getEndSelector = useMemo(() => endSelector(), []);
const getIsLoadingSelector = useMemo(() => isLoadingSelector(), []);
@ -154,6 +156,16 @@ const GraphOverlayComponent: React.FC<OwnProps> = ({ isEventViewer, timelineId }
[globalFullScreen, timelineId, timelineFullScreen]
);
const isInTimeline = timelineId === TimelineId.active;
const onCloseOverlay = useCallback(() => {
if (timelineId === TimelineId.active) {
setTimelineFullScreen(false);
} else {
setGlobalFullScreen(false);
}
dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: '' }));
}, [dispatch, timelineId, setTimelineFullScreen, setGlobalFullScreen]);
const toggleFullScreen = useCallback(() => {
if (timelineId === TimelineId.active) {
setTimelineFullScreen(!timelineFullScreen);
@ -173,41 +185,71 @@ const GraphOverlayComponent: React.FC<OwnProps> = ({ isEventViewer, timelineId }
[]
);
const existingIndexNames = useDeepEqualSelector<string[]>(existingIndexNamesSelector);
return (
<OverlayContainer
data-test-subj="overlayContainer"
$restrictWidth={isEventViewer && fullScreen}
>
<EuiHorizontalRule margin="none" />
<EuiFlexGroup gutterSize="none" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<Navigation
fullScreen={fullScreen}
globalFullScreen={globalFullScreen}
onCloseOverlay={onCloseOverlay}
timelineId={timelineId}
timelineFullScreen={timelineFullScreen}
toggleFullScreen={toggleFullScreen}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule margin="none" />
{graphEventId !== undefined ? (
<StyledResolver
databaseDocumentID={graphEventId}
resolverComponentInstanceID={timelineId}
indices={existingIndexNames}
shouldUpdate={shouldUpdate}
filters={{ from, to }}
/>
) : (
<EuiFlexGroup alignItems="center" justifyContent="center" style={{ height: '100%' }}>
<EuiLoadingSpinner size="xl" />
if (fullScreen && !isInTimeline) {
return (
<FullScreenOverlayContainer data-test-subj="overlayContainer">
<EuiHorizontalRule margin="none" />
<EuiFlexGroup gutterSize="none" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<Navigation
fullScreen={fullScreen}
globalFullScreen={globalFullScreen}
onCloseOverlay={onCloseOverlay}
timelineId={timelineId}
timelineFullScreen={timelineFullScreen}
toggleFullScreen={toggleFullScreen}
/>
</EuiFlexItem>
</EuiFlexGroup>
)}
</OverlayContainer>
);
<EuiHorizontalRule margin="none" />
{graphEventId !== undefined ? (
<StyledResolver
databaseDocumentID={graphEventId}
resolverComponentInstanceID={timelineId}
indices={existingIndexNames}
shouldUpdate={shouldUpdate}
filters={{ from, to }}
/>
) : (
<EuiFlexGroup alignItems="center" justifyContent="center" style={{ height: '100%' }}>
<EuiLoadingSpinner size="xl" />
</EuiFlexGroup>
)}
</FullScreenOverlayContainer>
);
} else {
return (
<OverlayContainer data-test-subj="overlayContainer">
<EuiHorizontalRule margin="none" />
<EuiFlexGroup gutterSize="none" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<Navigation
fullScreen={fullScreen}
globalFullScreen={globalFullScreen}
onCloseOverlay={onCloseOverlay}
timelineId={timelineId}
timelineFullScreen={timelineFullScreen}
toggleFullScreen={toggleFullScreen}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule margin="none" />
{graphEventId !== undefined ? (
<StyledResolver
databaseDocumentID={graphEventId}
resolverComponentInstanceID={timelineId}
indices={existingIndexNames}
shouldUpdate={shouldUpdate}
filters={{ from, to }}
/>
) : (
<EuiFlexGroup alignItems="center" justifyContent="center" style={{ height: '100%' }}>
<EuiLoadingSpinner size="xl" />
</EuiFlexGroup>
)}
</OverlayContainer>
);
}
};
export const GraphOverlay = React.memo(GraphOverlayComponent);

View file

@ -26,7 +26,7 @@ const GraphTabContentComponent: React.FC<GraphTabContentProps> = ({ timelineId }
return null;
}
return <GraphOverlay isEventViewer={false} timelineId={timelineId} />;
return <GraphOverlay timelineId={timelineId} />;
};
GraphTabContentComponent.displayName = 'GraphTabContentComponent';

View file

@ -8,7 +8,13 @@
import type { AlertConsumers as AlertConsumersTyped } from '@kbn/rule-data-utils';
// @ts-expect-error
import { AlertConsumers as AlertConsumersNonTyped } from '@kbn/rule-data-utils/target_node/alerts_as_data_rbac';
import { EuiEmptyPrompt, EuiLoadingContent, EuiPanel } from '@elastic/eui';
import {
EuiEmptyPrompt,
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiLoadingContent,
} from '@elastic/eui';
import { isEmpty } from 'lodash/fp';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import styled from 'styled-components';
@ -80,6 +86,16 @@ const EventsContainerLoading = styled.div.attrs(({ className = '' }) => ({
flex-direction: column;
`;
const FullWidthFlexGroup = styled(EuiFlexGroup)<{ $visible: boolean }>`
overflow: hidden;
margin: 0;
display: ${({ $visible }) => ($visible ? 'flex' : 'none')};
`;
const ScrollableFlexItem = styled(EuiFlexItem)`
overflow: auto;
`;
const SECURITY_ALERTS_CONSUMERS = [AlertConsumers.SIEM];
export interface TGridIntegratedProps {
@ -309,56 +325,61 @@ const TGridIntegratedComponent: React.FC<TGridIntegratedProps> = ({
</UpdatedFlexGroup>
{!graphEventId && graphOverlay == null && (
<>
{totalCountMinusDeleted === 0 && loading === false && (
<EuiEmptyPrompt
title={
<h2>
<FormattedMessage
id="xpack.timelines.tGrid.noResultsMatchSearchCriteriaTitle"
defaultMessage="No results match your search criteria"
/>
</h2>
}
titleSize="s"
body={
<p>
<FormattedMessage
id="xpack.timelines.tGrid.noResultsMatchSearchCriteriaDescription"
defaultMessage="Try searching over a longer period of time or modifying your search."
/>
</p>
}
/>
)}
{totalCountMinusDeleted > 0 && (
<StatefulBody
hasAlertsCrud={hasAlertsCrud}
activePage={pageInfo.activePage}
browserFields={browserFields}
filterQuery={filterQuery}
data={nonDeletedEvents}
defaultCellActions={defaultCellActions}
id={id}
isEventViewer={true}
itemsPerPageOptions={itemsPerPageOptions}
loadPage={loadPage}
onRuleChange={onRuleChange}
pageSize={itemsPerPage}
renderCellValue={renderCellValue}
rowRenderers={rowRenderers}
tabType={TimelineTabs.query}
tableView={tableView}
totalItems={totalCountMinusDeleted}
unit={unit}
filterStatus={filterStatus}
leadingControlColumns={leadingControlColumns}
trailingControlColumns={trailingControlColumns}
refetch={refetch}
indexNames={indexNames}
/>
)}
</>
<FullWidthFlexGroup
$visible={!graphEventId && graphOverlay == null}
gutterSize="none"
>
<ScrollableFlexItem grow={1}>
{totalCountMinusDeleted === 0 && loading === false && (
<EuiEmptyPrompt
title={
<h2>
<FormattedMessage
id="xpack.timelines.tGrid.noResultsMatchSearchCriteriaTitle"
defaultMessage="No results match your search criteria"
/>
</h2>
}
titleSize="s"
body={
<p>
<FormattedMessage
id="xpack.timelines.tGrid.noResultsMatchSearchCriteriaDescription"
defaultMessage="Try searching over a longer period of time or modifying your search."
/>
</p>
}
/>
)}
{totalCountMinusDeleted > 0 && (
<StatefulBody
hasAlertsCrud={hasAlertsCrud}
activePage={pageInfo.activePage}
browserFields={browserFields}
filterQuery={filterQuery}
data={nonDeletedEvents}
defaultCellActions={defaultCellActions}
id={id}
isEventViewer={true}
itemsPerPageOptions={itemsPerPageOptions}
loadPage={loadPage}
onRuleChange={onRuleChange}
pageSize={itemsPerPage}
renderCellValue={renderCellValue}
rowRenderers={rowRenderers}
tabType={TimelineTabs.query}
tableView={tableView}
totalItems={totalCountMinusDeleted}
unit={unit}
filterStatus={filterStatus}
leadingControlColumns={leadingControlColumns}
trailingControlColumns={trailingControlColumns}
refetch={refetch}
indexNames={indexNames}
/>
)}
</ScrollableFlexItem>
</FullWidthFlexGroup>
)}
</EventsContainerLoading>
)}