[Security Solution] Add analyzer to expandable flyout (#153709)

## Summary

This PR build on previously merged PR
(https://github.com/elastic/kibana/pull/152150), and adds analyzer to
the left section of expandable flyout under `Visualize`->`Analyzer
Graph`.


![image](https://user-images.githubusercontent.com/18648970/227638025-293c2b56-3b40-460d-92e7-0ccbfdbfecf9.png)

**How to test**

- add `xpack.securitySolution.enableExperimental:
['securityFlyoutEnabled']` to the `kibana.dev.json` file
- go to the Alerts page, and click on the expand detail button on any
row of the table
- then click on the expand details button in the flyout's header
- click `Visualize`, then `Analyzer Graph`

### Checklist

Delete any items that are not applicable to this PR.

- [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://github.com/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

---------

Co-authored-by: PhilippeOberti <philippe.oberti@elastic.co>
This commit is contained in:
christineweng 2023-03-29 17:50:26 -05:00 committed by GitHub
parent 9517d067b5
commit d210aff066
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 290 additions and 19 deletions

View file

@ -0,0 +1,44 @@
/*
* 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 { ANALYZER_NODE } from '../../../screens/alerts';
import { DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_GRAPH_ANALYZER_CONTENT } from '../../../screens/document_expandable_flyout';
import {
expandFirstAlertExpandableFlyout,
openGraphAnalyzer,
expandDocumentDetailsExpandableFlyoutLeftSection,
} from '../../../tasks/document_expandable_flyout';
import { cleanKibana } from '../../../tasks/common';
import { login, visit } from '../../../tasks/login';
import { createRule } from '../../../tasks/api_calls/rules';
import { getNewRule } from '../../../objects/rule';
import { ALERTS_URL } from '../../../urls/navigation';
import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule';
// Skipping these for now as the feature is protected behind a feature flag set to false by default
// To run the tests locally, add 'securityFlyoutEnabled' in the Cypress config.ts here https://github.com/elastic/kibana/blob/main/x-pack/test/security_solution_cypress/config.ts#L50
describe.skip(
'Alert details expandable flyout left panel analyzer graph',
{ testIsolation: false },
() => {
before(() => {
cleanKibana();
login();
createRule(getNewRule());
visit(ALERTS_URL);
waitForAlertsToPopulate();
expandFirstAlertExpandableFlyout();
expandDocumentDetailsExpandableFlyoutLeftSection();
openGraphAnalyzer();
});
it('should display analyzer graph and node list', () => {
cy.get(DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_GRAPH_ANALYZER_CONTENT).should('be.visible');
cy.get(ANALYZER_NODE).first().should('be.visible');
});
}
);

View file

@ -0,0 +1,76 @@
/*
* 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 { Provider as ReduxStoreProvider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import { MemoryRouter } from 'react-router-dom';
import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
import type { CoreStart } from '@kbn/core/public';
import { sourcererReducer } from '../../../common/store/sourcerer';
import { inputsReducer } from '../../../common/store/inputs';
import type { LeftPanelContext } from '../context';
import { LeftFlyoutContext } from '../context';
import { AnalyzeGraph } from './analyze_graph';
export default {
component: AnalyzeGraph,
title: 'Flyout/AnalyzeGraph',
};
// TODO to get this working, we need to spent some time getting all the foundation items for storybook
// (ReduxStoreProvider, CellActionsProvider...) similarly to how it was done for the TestProvidersComponent
// see ticket https://github.com/elastic/security-team/issues/6223
// export const Default: Story<void> = () => {
// const contextValue = {
// eventId: 'some_id',
// } as unknown as LeftPanelContext;
//
// return (
// <LeftFlyoutContext.Provider value={contextValue}>
// <AnalyzeGraph />
// </LeftFlyoutContext.Provider>
// );
// };
export const Error: Story<void> = () => {
const store = configureStore({
reducer: {
inputs: inputsReducer,
sourcerer: sourcererReducer,
},
});
const services = {
data: {},
notifications: {
toasts: {
addError: () => {},
addSuccess: () => {},
addWarning: () => {},
remove: () => {},
},
},
} as unknown as CoreStart;
const KibanaReactContext = createKibanaReactContext({ ...services });
const contextValue = {
eventId: null,
} as unknown as LeftPanelContext;
return (
<MemoryRouter>
<ReduxStoreProvider store={store}>
<KibanaReactContext.Provider>
<LeftFlyoutContext.Provider value={contextValue}>
<AnalyzeGraph />
</LeftFlyoutContext.Provider>
</KibanaReactContext.Provider>
</ReduxStoreProvider>
</MemoryRouter>
);
};

View file

@ -0,0 +1,66 @@
/*
* 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 '@testing-library/jest-dom';
import type { LeftPanelContext } from '../context';
import { LeftFlyoutContext } from '../context';
import { TestProviders } from '../../../common/mock';
import { AnalyzeGraph } from './analyze_graph';
import { ANALYZE_GRAPH_ERROR_TEST_ID, ANALYZER_GRAPH_TEST_ID } from './test_ids';
jest.mock('react-router-dom', () => {
const actual = jest.requireActual('react-router-dom');
return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) };
});
jest.mock('../../../resolver/view/use_resolver_query_params_cleaner');
const mockDispatch = jest.fn();
jest.mock('react-redux', () => {
const original = jest.requireActual('react-redux');
return {
...original,
useDispatch: () => mockDispatch,
};
});
describe('<AnalyzeGraph />', () => {
it('renders analyzer graph correctly', () => {
const contextValue = {
eventId: 'eventId',
} as unknown as LeftPanelContext;
const wrapper = render(
<TestProviders>
<LeftFlyoutContext.Provider value={contextValue}>
<AnalyzeGraph />
</LeftFlyoutContext.Provider>
</TestProviders>
);
expect(wrapper.getByTestId(ANALYZER_GRAPH_TEST_ID)).toBeInTheDocument();
});
it('should render error message on null eventId', () => {
const contextValue = {
eventId: null,
} as unknown as LeftPanelContext;
const wrapper = render(
<TestProviders>
<LeftFlyoutContext.Provider value={contextValue}>
<AnalyzeGraph />
</LeftFlyoutContext.Provider>
</TestProviders>
);
expect(wrapper.getByTestId(ANALYZE_GRAPH_ERROR_TEST_ID)).toBeInTheDocument();
expect(wrapper.getByText('Unable to display analyzer')).toBeInTheDocument();
expect(wrapper.getByText('There was an error displaying analyzer')).toBeInTheDocument();
});
});

View file

@ -7,8 +7,15 @@
import type { FC } from 'react';
import React from 'react';
import { EuiText } from '@elastic/eui';
import { ANALYZER_GRAPH_TEST_ID } from './test_ids';
import { EuiEmptyPrompt } from '@elastic/eui';
import { ANALYZER_ERROR_MESSAGE } from './translations';
import { useLeftPanelContext } from '../context';
import { ANALYZE_GRAPH_ERROR_TEST_ID, ANALYZER_GRAPH_TEST_ID } from './test_ids';
import { Resolver } from '../../../resolver/view';
import { useTimelineDataFilters } from '../../../timelines/containers/use_timeline_data_filters';
import { ERROR_TITLE, ERROR_MESSAGE } from '../../shared/translations';
import { isActiveTimeline } from '../../../helpers';
export const ANALYZE_GRAPH_ID = 'analyze_graph';
@ -16,7 +23,35 @@ export const ANALYZE_GRAPH_ID = 'analyze_graph';
* Analyzer graph view displayed in the document details expandable flyout left section under the Visualize tab
*/
export const AnalyzeGraph: FC = () => {
return <EuiText data-test-subj={ANALYZER_GRAPH_TEST_ID}>{'Analyzer graph'}</EuiText>;
const { eventId } = useLeftPanelContext();
const scopeId = 'fly-out';
const { from, to, shouldUpdate, selectedPatterns } = useTimelineDataFilters(
isActiveTimeline(scopeId)
);
if (!eventId) {
return (
<EuiEmptyPrompt
iconType="error"
color="danger"
title={<h2>{ERROR_TITLE(ANALYZER_ERROR_MESSAGE)}</h2>}
body={<p>{ERROR_MESSAGE(ANALYZER_ERROR_MESSAGE)}</p>}
data-test-subj={ANALYZE_GRAPH_ERROR_TEST_ID}
/>
);
}
return (
<div data-test-subj={ANALYZER_GRAPH_TEST_ID}>
<Resolver
databaseDocumentID={eventId}
resolverComponentInstanceID={scopeId}
indices={selectedPatterns}
shouldUpdate={shouldUpdate}
filters={{ from, to }}
/>
</div>
);
};
AnalyzeGraph.displayName = 'AnalyzeGraph';

View file

@ -6,4 +6,6 @@
*/
export const ANALYZER_GRAPH_TEST_ID = 'securitySolutionDocumentDetailsFlyoutAnalyzerGraph';
export const ANALYZE_GRAPH_ERROR_TEST_ID =
'securitySolutionDocumentDetailsFlyoutAnalyzerGraphError';
export const SESSION_VIEW_TEST_ID = 'securitySolutionDocumentDetailsFlyoutSessionView';

View file

@ -0,0 +1,15 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const ANALYZER_ERROR_MESSAGE = i18n.translate(
'xpack.securitySolution.flyout.analyzerErrorTitle',
{
defaultMessage: 'analyzer',
}
);

View file

@ -6,7 +6,7 @@
*/
import type { FC } from 'react';
import React, { memo, useState } from 'react';
import React, { memo, useState, useCallback } from 'react';
import { EuiButtonGroup, EuiSpacer } from '@elastic/eui';
import type { EuiButtonGroupOptionProps } from '@elastic/eui/src/components/button/button_group/button_group';
import {
@ -21,6 +21,8 @@ import {
VISUALIZE_BUTTONGROUP_OPTIONS,
} from './translations';
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';
const visualizeButtons: EuiButtonGroupOptionProps[] = [
{
@ -40,9 +42,16 @@ const visualizeButtons: EuiButtonGroupOptionProps[] = [
*/
export const VisualizeTab: FC = memo(() => {
const [activeVisualizationId, setActiveVisualizationId] = useState(SESSION_VIEW_ID);
const onChangeCompressed = (optionId: string) => {
setActiveVisualizationId(optionId);
};
const { startTransaction } = useStartTransaction();
const onChangeCompressed = useCallback(
(optionId: string) => {
setActiveVisualizationId(optionId);
if (optionId === ANALYZE_GRAPH_ID) {
startTransaction({ name: ALERTS_ACTIONS.OPEN_ANALYZER });
}
},
[startTransaction]
);
return (
<>

View file

@ -8,8 +8,9 @@
import { EuiEmptyPrompt } from '@elastic/eui';
import type { FC } from 'react';
import React, { memo } from 'react';
import { DOCUMENT_ERROR_DETAILS, DOCUMENT_ERROR_TITLE } from './translations';
import { JSON_TAB_ERROR_TEST_ID } from './test_ids';
import { ERROR_MESSAGE, ERROR_TITLE } from './translations';
import { ERROR_TITLE, ERROR_MESSAGE } from '../../shared/translations';
import { JsonView } from '../../../common/components/event_details/json_view';
import { useRightPanelContext } from '../context';
@ -24,8 +25,8 @@ export const JsonTab: FC = memo(() => {
<EuiEmptyPrompt
iconType="error"
color="danger"
title={<h2>{ERROR_TITLE}</h2>}
body={<p>{ERROR_MESSAGE}</p>}
title={<h2>{ERROR_TITLE(DOCUMENT_ERROR_TITLE)}</h2>}
body={<p>{ERROR_MESSAGE(DOCUMENT_ERROR_DETAILS)}</p>}
data-test-subj={JSON_TAB_ERROR_TEST_ID}
/>
);

View file

@ -8,7 +8,8 @@
import type { FC } from 'react';
import React, { memo } from 'react';
import { EuiEmptyPrompt } from '@elastic/eui';
import { ERROR_MESSAGE, ERROR_TITLE } from './translations';
import { DOCUMENT_ERROR_DETAILS, DOCUMENT_ERROR_TITLE } from './translations';
import { ERROR_TITLE, ERROR_MESSAGE } from '../../shared/translations';
import { TimelineTabs } from '../../../../common/types';
import { EventFieldsBrowser } from '../../../common/components/event_details/event_fields_browser';
import { useRightPanelContext } from '../context';
@ -25,8 +26,8 @@ export const TableTab: FC = memo(() => {
<EuiEmptyPrompt
iconType="error"
color="danger"
title={<h2>{ERROR_TITLE}</h2>}
body={<p>{ERROR_MESSAGE}</p>}
title={<h2>{ERROR_TITLE(DOCUMENT_ERROR_TITLE)}</h2>}
body={<p>{ERROR_MESSAGE(DOCUMENT_ERROR_DETAILS)}</p>}
data-test-subj={TABLE_TAB_ERROR_TEST_ID}
/>
);

View file

@ -7,14 +7,16 @@
import { i18n } from '@kbn/i18n';
export const ERROR_TITLE = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.errorTitle',
export const DOCUMENT_ERROR_TITLE = i18n.translate(
'xpack.securitySolution.flyout.documentErrorTitle',
{
defaultMessage: 'Unable to display document information',
defaultMessage: 'document information',
}
);
export const ERROR_MESSAGE = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.errorMessage',
{ defaultMessage: 'There was an error displaying the document fields and values' }
export const DOCUMENT_ERROR_DETAILS = i18n.translate(
'xpack.securitySolution.flyout.documentErrorMessage',
{
defaultMessage: 'the document fields and values',
}
);

View file

@ -0,0 +1,20 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const ERROR_TITLE = (title: string) =>
i18n.translate('xpack.securitySolution.flyout.errorTitle', {
values: { title },
defaultMessage: 'Unable to display {title}',
});
export const ERROR_MESSAGE = (message: string) =>
i18n.translate('xpack.securitySolution.flyout.errorMessage', {
values: { message },
defaultMessage: 'There was an error displaying {message}',
});