Skip to content

Commit

Permalink
[Security Solution]Analyzer in flyout Part 1 - add split mode in anal…
Browse files Browse the repository at this point in the history
…yzer and enable visualization tab (#192531)

## Summary

This PR is part 1 of moving analyzer to alerts flyout. To support
rendering the analyzer graph and details panel separately, this PR
introduced a `split` mode in analyzer:

- In split mode an additional `show panel` button is added in graph
control.

![image](https:/user-attachments/assets/5af3d387-69ac-4668-95d4-aedd53e897fb)


- default mode shows the panel as part of the graph (alert table
scenario)

![image](https:/user-attachments/assets/93a9f0ad-163b-45a3-9e7f-41d83e1cd85b)


## Update to analyzer state in redux

To support analyzer in alerts table and in flyout, the redux store for
alert/event table uses `scopeId` (`alerts-page` in screenshot), the
flyout analyzer uses `flyoutUrl-scopeId` (i.e.
`SecuritySolution-alerts-page`)

![image](https:/user-attachments/assets/c7f50866-642a-4e5d-9d80-a303f24934cb)


### Feature flag
This feature is currently behind a feature flag
`visualizationInFlyoutEnabled`
Note: this also enables session view in visualization section

### Video demo


https:/user-attachments/assets/6a95497e-1e94-42fe-a227-bbb9a7f0c303



### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https:/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
  • Loading branch information
christineweng authored Sep 19, 2024
1 parent 6030231 commit 2650cd0
Show file tree
Hide file tree
Showing 24 changed files with 725 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,11 @@ export const allowedExperimentalValues = Object.freeze({
*/
analyzerDatePickersAndSourcererDisabled: false,

/**
* Enables visualization: session viewer and analyzer in expandable flyout
*/
visualizationInFlyoutEnabled: false,

/**
* Enables an ability to customize Elastic prebuilt rules.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ export const mockGlobalState: State = {
[TableId.test]: EMPTY_RESOLVER,
[TimelineId.test]: EMPTY_RESOLVER,
[TimelineId.active]: EMPTY_RESOLVER,
flyout: EMPTY_RESOLVER,
[`securitySolution-${TableId.test}`]: EMPTY_RESOLVER,
},
sourcerer: {
...mockSourcererState,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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 { FlyoutPanelProps } from '@kbn/expandable-flyout';
import { FlyoutBody } from '@kbn/security-solution-common';
import type { DocumentDetailsAnalyzerPanelKey } from '../shared/constants/panel_keys';
import { DetailsPanel } from '../../../resolver/view/details_panel';

interface AnalyzerPanelProps extends Record<string, unknown> {
/**
* id to identify the scope of analyzer in redux
*/
resolverComponentInstanceID: string;
}

export interface AnalyzerPanelExpandableFlyoutProps extends FlyoutPanelProps {
key: typeof DocumentDetailsAnalyzerPanelKey;
params: AnalyzerPanelProps;
}

/**
* Displays node details panel for analyzer
*/
export const AnalyzerPanel: React.FC<AnalyzerPanelProps> = ({ resolverComponentInstanceID }) => {
return (
<FlyoutBody>
<div style={{ marginTop: '-15px' }}>
<DetailsPanel resolverComponentInstanceID={resolverComponentInstanceID} />
</div>
</FlyoutBody>
);
};

AnalyzerPanel.displayName = 'AnalyzerPanel';
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,25 @@
import React from 'react';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
import { TableId } from '@kbn/securitysolution-data-table';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { DocumentDetailsContext } from '../../shared/context';
import { TestProviders } from '../../../../common/mock';
import { AnalyzeGraph } from './analyze_graph';
import { AnalyzeGraph, ANALYZER_PREVIEW_BANNER } from './analyze_graph';
import { ANALYZER_GRAPH_TEST_ID } from './test_ids';
import { useWhichFlyout } from '../../shared/hooks/use_which_flyout';
import { mockFlyoutApi } from '../../shared/mocks/mock_flyout_context';
import { DocumentDetailsAnalyzerPanelKey } from '../../shared/constants/panel_keys';

jest.mock('react-router-dom', () => {
const actual = jest.requireActual('react-router-dom');
return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) };
});

jest.mock('@kbn/expandable-flyout');
jest.mock('../../../../resolver/view/use_resolver_query_params_cleaner');
jest.mock('../../shared/hooks/use_which_flyout');
const mockUseWhichFlyout = useWhichFlyout as jest.Mock;
const FLYOUT_KEY = 'securitySolution';

const mockDispatch = jest.fn();
jest.mock('react-redux', () => {
Expand All @@ -31,9 +39,15 @@ jest.mock('react-redux', () => {
});

describe('<AnalyzeGraph />', () => {
beforeEach(() => {
mockUseWhichFlyout.mockReturnValue(FLYOUT_KEY);
jest.mocked(useExpandableFlyoutApi).mockReturnValue(mockFlyoutApi);
});

it('renders analyzer graph correctly', () => {
const contextValue = {
eventId: 'eventId',
scopeId: TableId.test,
} as unknown as DocumentDetailsContext;

const wrapper = render(
Expand All @@ -45,4 +59,29 @@ describe('<AnalyzeGraph />', () => {
);
expect(wrapper.getByTestId(ANALYZER_GRAPH_TEST_ID)).toBeInTheDocument();
});

it('clicking view button should open details panel in preview', () => {
const contextValue = {
eventId: 'eventId',
scopeId: TableId.test,
} as unknown as DocumentDetailsContext;

const wrapper = render(
<TestProviders>
<DocumentDetailsContext.Provider value={contextValue}>
<AnalyzeGraph />
</DocumentDetailsContext.Provider>
</TestProviders>
);

expect(wrapper.getByTestId('resolver:graph-controls:show-panel-button')).toBeInTheDocument();
wrapper.getByTestId('resolver:graph-controls:show-panel-button').click();
expect(mockFlyoutApi.openPreviewPanel).toBeCalledWith({
id: DocumentDetailsAnalyzerPanelKey,
params: {
resolverComponentInstanceID: `${FLYOUT_KEY}-${TableId.test}`,
banner: ANALYZER_PREVIEW_BANNER,
},
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,62 @@
*/

import type { FC } from 'react';
import React, { useMemo } from 'react';

import React, { useMemo, useCallback } from 'react';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { i18n } from '@kbn/i18n';
import { useWhichFlyout } from '../../shared/hooks/use_which_flyout';
import { useDocumentDetailsContext } from '../../shared/context';
import { ANALYZER_GRAPH_TEST_ID } from './test_ids';
import { Resolver } from '../../../../resolver/view';
import { useTimelineDataFilters } from '../../../../timelines/containers/use_timeline_data_filters';
import { isActiveTimeline } from '../../../../helpers';
import { DocumentDetailsAnalyzerPanelKey } from '../../shared/constants/panel_keys';

export const ANALYZE_GRAPH_ID = 'analyze_graph';

export const ANALYZER_PREVIEW_BANNER = {
title: i18n.translate(
'xpack.securitySolution.flyout.left.visualizations.analyzer.panelPreviewTitle',
{
defaultMessage: 'Preview analyzer panels',
}
),
backgroundColor: 'warning',
textColor: 'warning',
};

/**
* Analyzer graph view displayed in the document details expandable flyout left section under the Visualize tab
*/
export const AnalyzeGraph: FC = () => {
const { eventId } = useDocumentDetailsContext();
const scopeId = 'flyout'; // Different scope Id to distinguish flyout and data table analyzers
const { eventId, scopeId } = useDocumentDetailsContext();
const key = useWhichFlyout() ?? 'memory';
const { from, to, shouldUpdate, selectedPatterns } = useTimelineDataFilters(
isActiveTimeline(scopeId)
);
const filters = useMemo(() => ({ from, to }), [from, to]);
const { openPreviewPanel } = useExpandableFlyoutApi();

// TODO as part of https:/elastic/security-team/issues/7032
// bring back no data message if needed
const onClick = useCallback(() => {
openPreviewPanel({
id: DocumentDetailsAnalyzerPanelKey,
params: {
resolverComponentInstanceID: `${key}-${scopeId}`,
banner: ANALYZER_PREVIEW_BANNER,
},
});
}, [openPreviewPanel, key, scopeId]);

return (
<div data-test-subj={ANALYZER_GRAPH_TEST_ID}>
<Resolver
databaseDocumentID={eventId}
resolverComponentInstanceID={scopeId}
resolverComponentInstanceID={`${key}-${scopeId}`}
indices={selectedPatterns}
shouldUpdate={shouldUpdate}
filters={filters}
isSplitPanel
showPanelOnClick={onClick}
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ export const LeftPanel: FC<Partial<DocumentDetailsProps>> = memo(({ path }) => {
'securitySolutionNotesEnabled'
);

const visualizationInFlyoutEnabled = useIsExperimentalFeatureEnabled(
'visualizationInFlyoutEnabled'
);

const tabsDisplayed = useMemo(() => {
const tabList =
eventKind === EventKind.signal
Expand All @@ -46,8 +50,11 @@ export const LeftPanel: FC<Partial<DocumentDetailsProps>> = memo(({ path }) => {
if (securitySolutionNotesEnabled && !isPreview) {
tabList.push(tabs.notesTab);
}
if (visualizationInFlyoutEnabled && !isPreview) {
return [tabs.visualizeTab, ...tabList];
}
return tabList;
}, [eventKind, isPreview, securitySolutionNotesEnabled]);
}, [eventKind, isPreview, securitySolutionNotesEnabled, visualizationInFlyoutEnabled]);

const selectedTabId = useMemo(() => {
const defaultTab = tabsDisplayed[0].id;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,22 @@ import { useExpandableFlyoutApi, useExpandableFlyoutState } from '@kbn/expandabl
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { useDocumentDetailsContext } from '../../shared/context';
import { DocumentDetailsLeftPanelKey } from '../../shared/constants/panel_keys';
import { useWhichFlyout } from '../../shared/hooks/use_which_flyout';
import {
DocumentDetailsLeftPanelKey,
DocumentDetailsAnalyzerPanelKey,
} from '../../shared/constants/panel_keys';
import { LeftPanelVisualizeTab } from '..';
import {
VISUALIZE_TAB_BUTTON_GROUP_TEST_ID,
VISUALIZE_TAB_GRAPH_ANALYZER_BUTTON_TEST_ID,
VISUALIZE_TAB_SESSION_VIEW_BUTTON_TEST_ID,
} from './test_ids';
import { ANALYZE_GRAPH_ID, AnalyzeGraph } from '../components/analyze_graph';
import {
ANALYZE_GRAPH_ID,
AnalyzeGraph,
ANALYZER_PREVIEW_BANNER,
} from '../components/analyze_graph';
import { SESSION_VIEW_ID, SessionView } from '../components/session_view';
import { ALERTS_ACTIONS } from '../../../../common/lib/apm/user_actions';
import { useStartTransaction } from '../../../../common/lib/apm/use_start_transaction';
Expand Down Expand Up @@ -52,11 +60,12 @@ const visualizeButtons: EuiButtonGroupOptionProps[] = [
*/
export const VisualizeTab = memo(() => {
const { eventId, indexName, scopeId } = useDocumentDetailsContext();
const { openLeftPanel } = useExpandableFlyoutApi();
const { openLeftPanel, openPreviewPanel } = useExpandableFlyoutApi();
const panels = useExpandableFlyoutState();
const [activeVisualizationId, setActiveVisualizationId] = useState(
panels.left?.path?.subTab ?? SESSION_VIEW_ID
);
const key = useWhichFlyout() ?? 'memory';
const { startTransaction } = useStartTransaction();
const onChangeCompressed = useCallback(
(optionId: string) => {
Expand All @@ -76,8 +85,15 @@ export const VisualizeTab = memo(() => {
scopeId,
},
});
openPreviewPanel({
id: DocumentDetailsAnalyzerPanelKey,
params: {
resolverComponentInstanceID: `${key}-${scopeId}`,
banner: ANALYZER_PREVIEW_BANNER,
},
});
},
[startTransaction, eventId, indexName, scopeId, openLeftPanel]
[startTransaction, eventId, indexName, scopeId, openLeftPanel, openPreviewPanel, key]
);

useEffect(() => {
Expand Down
Loading

0 comments on commit 2650cd0

Please sign in to comment.