Skip to content

Commit

Permalink
[Security Solution] Add analyzer to expandable flyout (#153709)
Browse files Browse the repository at this point in the history
## Summary

This PR build on previously merged PR
(#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 <[email protected]>
  • Loading branch information
christineweng and PhilippeOberti authored Mar 29, 2023
1 parent 9517d06 commit d210aff
Show file tree
Hide file tree
Showing 11 changed files with 290 additions and 19 deletions.
Original file line number Diff line number Diff line change
@@ -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');
});
}
);
Original file line number Diff line number Diff line change
@@ -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>
);
};
Original file line number Diff line number Diff line change
@@ -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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,51 @@

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';

/**
* 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';
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
@@ -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',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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[] = [
{
Expand All @@ -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 (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
);
Original file line number Diff line number Diff line change
@@ -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}',
});

0 comments on commit d210aff

Please sign in to comment.