Skip to content

Commit

Permalink
Merge pull request #3 from michaelolo24/integrate-session-view-with-d…
Browse files Browse the repository at this point in the history
…etails-flyout

[Security Solution][Investigations] - Integrate details flyout with session view
  • Loading branch information
kqualters-elastic authored Mar 25, 2022
2 parents 608f7c8 + 638bcfe commit 9b18ccd
Show file tree
Hide file tree
Showing 6 changed files with 349 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ import styled from 'styled-components';
import type { Filter } from '@kbn/es-query';
import { inputsModel, State } from '../../store';
import { inputsActions } from '../../store/actions';
import { ControlColumnProps, RowRenderer, TimelineId } from '../../../../common/types/timeline';
import {
ControlColumnProps,
RowRenderer,
TimelineId,
TimelineTabs,
} from '../../../../common/types/timeline';
import { APP_ID, APP_UI_ID } from '../../../../common/constants';
import { timelineActions } from '../../../timelines/store/timeline';
import type { SubsetTimelineModel } from '../../../timelines/store/timeline/model';
Expand All @@ -24,7 +29,6 @@ import { SourcererScopeName } from '../../store/sourcerer/model';
import { useSourcererDataView } from '../../containers/sourcerer';
import type { EntityType } from '../../../../../timelines/common';
import { TGridCellAction } from '../../../../../timelines/common/types';
import { DetailsPanel } from '../../../timelines/components/side_panel';
import { CellValueElementProps } from '../../../timelines/components/timeline/cell_rendering';
import { FIELDS_WITHOUT_CELL_ACTIONS } from '../../lib/cell_actions/constants';
import { useGetUserCasesPermissions, useKibana } from '../../lib/kibana';
Expand All @@ -33,6 +37,7 @@ import {
useFieldBrowserOptions,
FieldEditorActions,
} from '../../../timelines/components/fields_browser';
import { useDetailPanel } from '../../../timelines/components/side_panel/hooks/use_detail_panel';

const EMPTY_CONTROL_COLUMNS: ControlColumnProps[] = [];

Expand Down Expand Up @@ -105,8 +110,8 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
itemsPerPage,
itemsPerPageOptions,
kqlMode,
showCheckboxes,
sessionViewId,
showCheckboxes,
sort,
} = defaultModel,
} = useSelector((state: State) => eventsViewerSelector(state, id));
Expand Down Expand Up @@ -156,11 +161,22 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({

const globalFilters = useMemo(() => [...filters, ...(pageFilters ?? [])], [filters, pageFilters]);
const trailingControlColumns: ControlColumnProps[] = EMPTY_CONTROL_COLUMNS;

const { openDetailsPanel, DetailsPanel } = useDetailPanel({
isFlyoutView: true,
entityType,
sourcererScope: SourcererScopeName.timeline,
timelineId: id,
tabType: TimelineTabs.query,
});

const graphOverlay = useMemo(() => {
const shouldShowOverlay =
(graphEventId != null && graphEventId.length > 0) || sessionViewId !== null;
return shouldShowOverlay ? <GraphOverlay timelineId={id} /> : null;
}, [graphEventId, id, sessionViewId]);
return shouldShowOverlay ? (
<GraphOverlay timelineId={id} openDetailsPanel={openDetailsPanel} />
) : null;
}, [graphEventId, id, sessionViewId, openDetailsPanel]);
const setQuery = useCallback(
(inspect, loading, refetch) => {
dispatch(inputsActions.setQuery({ id, inputId: 'global', inspect, loading, refetch }));
Expand Down Expand Up @@ -240,14 +256,7 @@ const StatefulEventsViewerComponent: React.FC<Props> = ({
})}
</InspectButtonContainer>
</FullScreenContainer>
<DetailsPanel
browserFields={browserFields}
entityType={entityType}
docValueFields={docValueFields}
isFlyoutView
runtimeMappings={runtimeMappings}
timelineId={id}
/>
{DetailsPanel}
</CasesContext>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ const ScrollableFlexItem = styled(EuiFlexItem)`
width: 100%;
`;

interface OwnProps {
interface GraphOverlayProps {
openDetailsPanel: (eventId?: string, onClose?: () => void) => void;
timelineId: TimelineId;
}

Expand Down Expand Up @@ -132,7 +133,7 @@ NavigationComponent.displayName = 'NavigationComponent';

const Navigation = React.memo(NavigationComponent);

const GraphOverlayComponent: React.FC<OwnProps> = ({ timelineId }) => {
const GraphOverlayComponent: React.FC<GraphOverlayProps> = ({ timelineId, openDetailsPanel }) => {
const dispatch = useDispatch();
const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen();
const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen();
Expand All @@ -147,9 +148,12 @@ const GraphOverlayComponent: React.FC<OwnProps> = ({ timelineId }) => {
);
const sessionViewMain = useMemo(() => {
return sessionViewId !== null
? sessionView.getSessionView({ sessionEntityId: sessionViewId })
? sessionView.getSessionView({
sessionEntityId: sessionViewId,
loadAlertDetails: openDetailsPanel,
})
: null;
}, [sessionView, sessionViewId]);
}, [sessionView, sessionViewId, openDetailsPanel]);

const getStartSelector = useMemo(() => startSelector(), []);
const getEndSelector = useMemo(() => endSelector(), []);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* 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 { renderHook, act } from '@testing-library/react-hooks';
import { useDetailPanel, UseDetailPanelConfig } from './use_detail_panel';
import { timelineActions } from '../../../store/timeline';
import { useDeepEqualSelector } from '../../../../common/hooks/use_selector';
import { SourcererScopeName } from '../../../../common/store/sourcerer/model';
import { TimelineId, TimelineTabs } from '../../../../../common/types';

const mockDispatch = jest.fn();
jest.mock('../../../../common/lib/kibana');
jest.mock('../../../../common/hooks/use_selector');
jest.mock('../../../store/timeline');
jest.mock('react-redux', () => {
const original = jest.requireActual('react-redux');
return {
...original,
useDispatch: () => mockDispatch,
};
});
jest.mock('../../../../common/containers/sourcerer', () => {
const mockSourcererReturn = {
browserFields: {},
docValueFields: [],
loading: true,
indexPattern: {},
selectedPatterns: [],
missingPatterns: [],
};
return {
useSourcererDataView: jest.fn().mockReturnValue(mockSourcererReturn),
};
});

describe('useDetailPanel', () => {
const defaultProps: UseDetailPanelConfig = {
sourcererScope: SourcererScopeName.detections,
timelineId: TimelineId.test,
};
const mockGetExpandedDetail = jest.fn().mockImplementation(() => ({}));
beforeEach(() => {
(useDeepEqualSelector as jest.Mock).mockImplementation((cb) => {
return mockGetExpandedDetail();
});
});
afterEach(() => {
(useDeepEqualSelector as jest.Mock).mockClear();
});

test('should return openDetailsPanel fn, handleOnDetailsPanelClosed fn, shouldShowDetailsPanel, and the DetailsPanel component', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() => {
return useDetailPanel(defaultProps);
});
await waitForNextUpdate();

expect(result.current.openDetailsPanel).toBeDefined();
expect(result.current.handleOnDetailsPanelClosed).toBeDefined();
expect(result.current.shouldShowDetailsPanel).toBe(false);
expect(result.current.DetailsPanel).toBeNull();
});
});

test('should fire redux action to open details panel', async () => {
const testEventId = '123';
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() => {
return useDetailPanel(defaultProps);
});
await waitForNextUpdate();

result.current?.openDetailsPanel(testEventId);

expect(mockDispatch).toHaveBeenCalled();
expect(timelineActions.toggleDetailPanel).toHaveBeenCalled();
});
});

test('should call provided onClose callback provided to openDetailsPanel fn', async () => {
const testEventId = '123';
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() => {
return useDetailPanel(defaultProps);
});
await waitForNextUpdate();

const mockOnClose = jest.fn();
result.current?.openDetailsPanel(testEventId, mockOnClose);
result.current?.handleOnDetailsPanelClosed();

expect(mockOnClose).toHaveBeenCalled();
});
});

test('should call the last onClose callback provided to openDetailsPanel fn', async () => {
// Test that the onClose ref is properly updated
const testEventId = '123';
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() => {
return useDetailPanel(defaultProps);
});
await waitForNextUpdate();

const mockOnClose = jest.fn();
const secondMockOnClose = jest.fn();
result.current?.openDetailsPanel(testEventId, mockOnClose);
result.current?.handleOnDetailsPanelClosed();

expect(mockOnClose).toHaveBeenCalled();

result.current?.openDetailsPanel(testEventId, secondMockOnClose);
result.current?.handleOnDetailsPanelClosed();

expect(secondMockOnClose).toHaveBeenCalled();
});
});

test('should show the details panel', async () => {
mockGetExpandedDetail.mockImplementation(() => ({
[TimelineTabs.session]: {
panelView: 'somePanel',
},
}));
const updatedProps = {
...defaultProps,
tabType: TimelineTabs.session,
};

await act(async () => {
const { result, waitForNextUpdate } = renderHook(() => {
return useDetailPanel(updatedProps);
});
await waitForNextUpdate();

expect(result.current.DetailsPanel).toMatchInlineSnapshot(`
<Memo(DetailsPanel)
browserFields={Object {}}
docValueFields={Array []}
handleOnPanelClosed={[Function]}
tabType="session"
timelineId="test"
/>
`);
});
});
});
Loading

0 comments on commit 9b18ccd

Please sign in to comment.