Skip to content

Commit

Permalink
[Security Solution] expandable flyout json tab
Browse files Browse the repository at this point in the history
  • Loading branch information
PhilippeOberti committed Mar 9, 2023
1 parent 1e573da commit 8d45003
Show file tree
Hide file tree
Showing 12 changed files with 213 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
openJsonTab,
openOverviewTab,
openTableTab,
scrollWithinDocumentDetailsExpandableFlyoutRightSection,
} from '../../../tasks/document_expandable_flyout';
import { cleanKibana } from '../../../tasks/common';
import { login, visit } from '../../../tasks/login';
Expand Down Expand Up @@ -73,6 +74,9 @@ describe.skip('Alert details expandable flyout right panel', { testIsolation: fa
cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_CONTENT).should('be.visible');

openJsonTab();
// the json component is rendered within a dom element with overflow, so Cypress isn't finding it
// this next line is a hack that vertically scrolls down to ensure Cypress finds it
scrollWithinDocumentDetailsExpandableFlyoutRightSection(0, 6500);
cy.get(DOCUMENT_DETAILS_FLYOUT_JSON_TAB_CONTENT).should('be.visible');
});
});
2 changes: 1 addition & 1 deletion x-pack/plugins/security_solution/cypress/helpers/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ export const getDataTestSubjectSelector = (dataTestSubjectValue: string) =>
* Helper function to generate selector by class
* @param className the value passed to class property of the DOM element
*/
export const getClassSelector = (className: string) => `[.${className}]`;
export const getClassSelector = (className: string) => `.${className}`;
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ import {
} from '../../public/flyout/right/components/test_ids';
import { getDataTestSubjectSelector } from '../helpers/common';

/* Right section */

export const DOCUMENT_DETAILS_FLYOUT_HEADER_TITLE = getDataTestSubjectSelector(
FLYOUT_HEADER_TITLE_TEST_ID
);
Expand All @@ -61,6 +63,8 @@ export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_CONTENT =
export const DOCUMENT_DETAILS_FLYOUT_JSON_TAB_CONTENT =
getDataTestSubjectSelector(JSON_TAB_CONTENT_TEST_ID);

/* Left section */

export const DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB =
getDataTestSubjectSelector(VISUALIZE_TAB_TEST_ID);
export const DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_BUTTON,
} from '../screens/document_expandable_flyout';
import { EXPAND_ALERT_BTN } from '../screens/alerts';
import { getClassSelector } from '../helpers/common';

/**
* Find the first alert row in the alerts table then click on the expand icon button to open the flyout
Expand All @@ -39,6 +40,13 @@ export const expandDocumentDetailsExpandableFlyoutLeftSection = () =>
export const collapseDocumentDetailsExpandableFlyoutLeftSection = () =>
cy.get(DOCUMENT_DETAILS_FLYOUT_COLLAPSE_DETAILS_BUTTON).should('be.visible').click();

/**
* Scroll to x-y positions within the right section of the document details expandable flyout
* // TODO revisit this as it seems very fragile: the first element found is the timeline flyout, which isn't visible but still exist in the DOM
*/
export const scrollWithinDocumentDetailsExpandableFlyoutRightSection = (x: number, y: number) =>
cy.get(getClassSelector('euiFlyout')).last().scrollTo(x, y);

/**
* Open the Overview tab in the document details expandable flyout right section
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const Expand: Story<void> = () => {
const panelContextValue = {
eventId: 'eventId',
indexName: 'indexName',
};
} as unknown as RightPanelContext;

return (
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe('<ExpandDetailButton />', () => {
const panelContextValue = {
eventId: 'eventId',
indexName: 'indexName',
};
} as unknown as RightPanelContext;

const { getByTestId } = render(
<ExpandableFlyoutContext.Provider value={flyoutContextValue}>
Expand Down
64 changes: 62 additions & 2 deletions x-pack/plugins/security_solution/public/flyout/right/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,18 @@
* 2.0.
*/

import type { BrowserFields, TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
import { css } from '@emotion/react';
import React, { createContext, useContext, useMemo } from 'react';
import type { SearchHit } from '@kbn/es-types';
import { EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
import { useTimelineEventsDetails } from '../../timelines/containers/details';
import { getAlertIndexAlias } from '../../timelines/components/side_panel/event_details/helpers';
import { useSpaceId } from '../../common/hooks/use_space_id';
import { useRouteSpy } from '../../common/utils/route/use_route_spy';
import { SecurityPageName } from '../../../common/constants';
import { SourcererScopeName } from '../../common/store/sourcerer/model';
import { useSourcererDataView } from '../../common/containers/sourcerer';
import type { RightPanelProps } from '.';

export interface RightPanelContext {
Expand All @@ -17,6 +28,18 @@ export interface RightPanelContext {
* Name of the index used in the parent's page
*/
indexName: string;
/**
* An object containing fields by type
*/
browserFields: BrowserFields | null;
/**
* An array of field objects with category and value
*/
dataFormattedForFieldBrowser: TimelineEventsDetailsItem[] | null;
/**
* The actual raw document object
*/
searchHit: SearchHit<object> | undefined;
}

export const RightPanelContext = createContext<RightPanelContext | undefined>(undefined);
Expand All @@ -29,11 +52,48 @@ export type RightPanelProviderProps = {
} & Partial<RightPanelProps['params']>;

export const RightPanelProvider = ({ id, indexName, children }: RightPanelProviderProps) => {
const currentSpaceId = useSpaceId();
const eventIndex = indexName ? getAlertIndexAlias(indexName, currentSpaceId) ?? indexName : '';
const [{ pageName }] = useRouteSpy();
const sourcererScope =
pageName === SecurityPageName.detections
? SourcererScopeName.detections
: SourcererScopeName.default;
const sourcererDataView = useSourcererDataView(sourcererScope);
const [loading, dataFormattedForFieldBrowser, searchHit] = useTimelineEventsDetails({
indexName: eventIndex,
eventId: id ?? '',
runtimeMappings: sourcererDataView.runtimeMappings,
skip: !id,
});

const contextValue = useMemo(
() => (id && indexName ? { eventId: id, indexName } : undefined),
[id, indexName]
() =>
id && indexName
? {
eventId: id,
indexName,
browserFields: sourcererDataView.browserFields,
dataFormattedForFieldBrowser,
searchHit: searchHit as SearchHit<object>,
}
: undefined,
[id, indexName, sourcererDataView.browserFields, dataFormattedForFieldBrowser, searchHit]
);

if (loading) {
return (
<EuiFlexItem
css={css`
align-items: center;
justify-content: center;
`}
>
<EuiLoadingSpinner size="xxl" />
</EuiFlexItem>
);
}

return <RightPanelContext.Provider value={contextValue}>{children}</RightPanelContext.Provider>;
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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 { RightPanelContext } from '../context';
import { JsonTab } from './json_tab';

export default {
component: JsonTab,
title: 'Flyout/JsonTab',
};

export const Default: Story<void> = () => {
const contextValue = {
searchHit: {
some_field: 'some_value',
},
} as unknown as RightPanelContext;

return (
<RightPanelContext.Provider value={contextValue}>
<JsonTab />
</RightPanelContext.Provider>
);
};

export const Error: Story<void> = () => {
const contextValue = {
searchHit: null,
} as unknown as RightPanelContext;

return (
<RightPanelContext.Provider value={contextValue}>
<JsonTab />
</RightPanelContext.Provider>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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 { RightPanelContext } from '../context';
import { JsonTab } from './json_tab';
import { JSON_TAB_ERROR_TEST_ID, JSON_TAB_CONTENT_TEST_ID } from './test_ids';

describe('<JsonTab />', () => {
it('should render code block component', () => {
const contextValue = {
searchHit: {
some_field: 'some_value',
},
} as unknown as RightPanelContext;

const { getByTestId } = render(
<RightPanelContext.Provider value={contextValue}>
<JsonTab />
</RightPanelContext.Provider>
);

expect(getByTestId(JSON_TAB_CONTENT_TEST_ID)).toBeInTheDocument();
});

it('should render error message on invalid searchHit', () => {
const contextValue = {
searchHit: null,
} as unknown as RightPanelContext;

const { getByTestId, getByText } = render(
<RightPanelContext.Provider value={contextValue}>
<JsonTab />
</RightPanelContext.Provider>
);

expect(getByTestId(JSON_TAB_ERROR_TEST_ID)).toBeInTheDocument();
expect(getByText('Unable to display document information')).toBeInTheDocument();
expect(
getByText('There was an error displaying the document fields and values')
).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,33 @@
* 2.0.
*/

import { EuiText } from '@elastic/eui';
import { EuiEmptyPrompt } from '@elastic/eui';
import type { FC } from 'react';
import React, { memo } from 'react';
import { JSON_TAB_CONTENT_TEST_ID } from './test_ids';
import { JSON_TAB_ERROR_TEST_ID } from './test_ids';
import { ERROR_MESSAGE, ERROR_TITLE } from './translations';
import { JsonView } from '../../../common/components/event_details/json_view';
import { useRightPanelContext } from '../context';

/**
* Json view displayed in the document details expandable flyout right section
*/
export const JsonTab: FC = memo(() => {
return <EuiText data-test-subj={JSON_TAB_CONTENT_TEST_ID}>{'Json tab'}</EuiText>;
const { searchHit } = useRightPanelContext();

if (!searchHit) {
return (
<EuiEmptyPrompt
iconType="error"
color="danger"
title={<h2>{ERROR_TITLE}</h2>}
body={<p>{ERROR_MESSAGE}</p>}
data-test-subj={JSON_TAB_ERROR_TEST_ID}
/>
);
}

return <JsonView rawEventData={searchHit} />;
});

JsonTab.displayName = 'JsonTab';
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@
export const OVERVIEW_TAB_CONTENT_TEST_ID =
'securitySolutionDocumentDetailsFlyoutOverviewTabContent';
export const TABLE_TAB_CONTENT_TEST_ID = 'securitySolutionDocumentDetailsFlyoutTableTabContent';
export const JSON_TAB_CONTENT_TEST_ID = 'securitySolutionDocumentDetailsFlyoutJsonTabContent';
export const JSON_TAB_CONTENT_TEST_ID = 'jsonView';
export const JSON_TAB_ERROR_TEST_ID = 'securitySolutionDocumentDetailsFlyoutJsonTabError';
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 = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.errorTitle',
{
defaultMessage: 'Unable to display document information',
}
);

export const ERROR_MESSAGE = i18n.translate(
'xpack.securitySolution.flyout.documentDetails.errorMessage',
{ defaultMessage: 'There was an error displaying the document fields and values' }
);

0 comments on commit 8d45003

Please sign in to comment.