diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_right_panel.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_right_panel.cy.ts index 6b3be87473dae..22375c1cc9b3d 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_right_panel.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_right_panel.cy.ts @@ -14,6 +14,7 @@ import { DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB, DOCUMENT_DETAILS_FLYOUT_TABLE_TAB, DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_CONTENT, + DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_EVENT_TYPE_ROW, } from '../../../screens/document_expandable_flyout'; import { collapseDocumentDetailsExpandableFlyoutLeftSection, @@ -70,8 +71,14 @@ describe.skip('Alert details expandable flyout right panel', { testIsolation: fa // we shouldn't need to test anything here as it's covered with the new overview_tab file openTableTab(); + // the table component is rendered within a dom element with overflow, so Cypress isn't finding it + // this next line is a hack that scrolls to a specific element in the table to ensure Cypress finds it + cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_EVENT_TYPE_ROW).scrollIntoView(); cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_CONTENT).should('be.visible'); + // scroll back up to the top to open the json tab + cy.get(DOCUMENT_DETAILS_FLYOUT_JSON_TAB).scrollIntoView(); + 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 diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_right_panel_table_tab.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_right_panel_table_tab.cy.ts new file mode 100644 index 0000000000000..bbe2c6356177a --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_right_panel_table_tab.cy.ts @@ -0,0 +1,78 @@ +/* + * 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 { closeTimeline, openActiveTimeline } from '../../../tasks/timeline'; +import { PROVIDER_BADGE } from '../../../screens/timeline'; +import { removeKqlFilter } from '../../../tasks/search_bar'; +import { FILTER_BADGE } from '../../../screens/alerts'; +import { + DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ID_ROW, + DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_COPY_TO_CLIPBOARD, + DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_TIMESTAMP_ROW, +} from '../../../screens/document_expandable_flyout'; +import { + addToTimelineTableTabTable, + clearFilterTableTabTable, + copyToClipboardTableTabTable, + expandFirstAlertExpandableFlyout, + filterInTableTabTable, + filterOutTableTabTable, + filterTableTabTable, + openTableTab, +} 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 right panel', { testIsolation: false }, () => { + before(() => { + cleanKibana(); + login(); + createRule(getNewRule()); + visit(ALERTS_URL); + waitForAlertsToPopulate(); + expandFirstAlertExpandableFlyout(); + openTableTab(); + }); + + it('should display and filter the table', () => { + cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_TIMESTAMP_ROW).should('be.visible'); + cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ID_ROW).should('be.visible'); + filterTableTabTable('timestamp'); + cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_TIMESTAMP_ROW).should('be.visible'); + clearFilterTableTabTable(); + }); + + it('should test filter in cell actions', () => { + filterInTableTabTable(); + cy.get(FILTER_BADGE).first().should('contain.text', '@timestamp:'); + removeKqlFilter(); + }); + + it('should test filter out cell actions', () => { + filterOutTableTabTable(); + cy.get(FILTER_BADGE).first().should('contain.text', 'NOT @timestamp:'); + removeKqlFilter(); + }); + + it('should test add to timeline cell actions', () => { + addToTimelineTableTabTable(); + openActiveTimeline(); + cy.get(PROVIDER_BADGE).first().should('contain.text', '@timestamp'); + closeTimeline(); + }); + + it('should test copy to clipboard cell actions', () => { + copyToClipboardTableTabTable(); + cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_COPY_TO_CLIPBOARD).should('be.visible'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/screens/document_expandable_flyout.ts b/x-pack/plugins/security_solution/cypress/screens/document_expandable_flyout.ts index e1f2588d38b9a..9af6f601c6a14 100644 --- a/x-pack/plugins/security_solution/cypress/screens/document_expandable_flyout.ts +++ b/x-pack/plugins/security_solution/cypress/screens/document_expandable_flyout.ts @@ -24,6 +24,7 @@ import { VISUALIZE_TAB_TEST_ID, } from '../../public/flyout/left/test_ids'; import { + FLYOUT_BODY_TEST_ID, JSON_TAB_TEST_ID, OVERVIEW_TAB_TEST_ID, TABLE_TAB_TEST_ID, @@ -39,10 +40,11 @@ import { MITRE_ATTACK_DETAILS_TEST_ID, MITRE_ATTACK_TITLE_TEST_ID, } from '../../public/flyout/right/components/test_ids'; -import { getDataTestSubjectSelector } from '../helpers/common'; +import { getClassSelector, getDataTestSubjectSelector } from '../helpers/common'; /* Right section */ +export const DOCUMENT_DETAILS_FLYOUT_BODY = getDataTestSubjectSelector(FLYOUT_BODY_TEST_ID); export const DOCUMENT_DETAILS_FLYOUT_HEADER_TITLE = getDataTestSubjectSelector( FLYOUT_HEADER_TITLE_TEST_ID ); @@ -92,9 +94,39 @@ export const DOCUMENT_DETAILS_FLYOUT_INVESTIGATIONS_TAB_CONTENT = getDataTestSub export const DOCUMENT_DETAILS_FLYOUT_HISTORY_TAB_CONTENT = getDataTestSubjectSelector( HISTORY_TAB_CONTENT_TEST_ID ); + +/* Overview tab */ + export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_MITRE_ATTACK_TITLE = getDataTestSubjectSelector( MITRE_ATTACK_TITLE_TEST_ID ); export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_MITRE_ATTACK_DETAILS = getDataTestSubjectSelector( MITRE_ATTACK_DETAILS_TEST_ID ); + +/* Table tab */ + +export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_FILTER = getClassSelector('euiFieldSearch'); +export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_CLEAR_FILTER = + getDataTestSubjectSelector('clearSearchButton'); +export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_TIMESTAMP_ROW = getDataTestSubjectSelector( + 'event-fields-table-row-@timestamp' +); +export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ID_ROW = getDataTestSubjectSelector( + 'event-fields-table-row-_id' +); +export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_EVENT_TYPE_ROW = getDataTestSubjectSelector( + 'event-fields-table-row-event.type' +); +export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_FILTER_IN = getDataTestSubjectSelector( + 'actionItem-security-detailsFlyout-cellActions-filterIn' +); +export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_FILTER_OUT = getDataTestSubjectSelector( + 'actionItem-security-detailsFlyout-cellActions-filterOut' +); +export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_MORE_ACTIONS = + getDataTestSubjectSelector('showExtraActionsButton'); +export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_ADD_TO_TIMELINE = + getDataTestSubjectSelector('actionItem-security-detailsFlyout-cellActions-addToTimeline'); +export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_COPY_TO_CLIPBOARD = + getDataTestSubjectSelector('actionItem-security-detailsFlyout-cellActions-copyToClipboard'); diff --git a/x-pack/plugins/security_solution/cypress/tasks/document_expandable_flyout.ts b/x-pack/plugins/security_solution/cypress/tasks/document_expandable_flyout.ts index 7ff7edcda7ba4..e66d1117c6603 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/document_expandable_flyout.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/document_expandable_flyout.ts @@ -6,6 +6,7 @@ */ import { + DOCUMENT_DETAILS_FLYOUT_BODY, DOCUMENT_DETAILS_FLYOUT_COLLAPSE_DETAILS_BUTTON, DOCUMENT_DETAILS_FLYOUT_EXPAND_DETAILS_BUTTON, DOCUMENT_DETAILS_FLYOUT_HISTORY_TAB, @@ -14,6 +15,12 @@ import { DOCUMENT_DETAILS_FLYOUT_JSON_TAB, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB, DOCUMENT_DETAILS_FLYOUT_TABLE_TAB, + DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_CLEAR_FILTER, + DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_FILTER, + DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_ADD_TO_TIMELINE, + DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_FILTER_IN, + DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_FILTER_OUT, + DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_MORE_ACTIONS, DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB, DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_GRAPH_ANALYZER_BUTTON, DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_BUTTON, @@ -100,3 +107,62 @@ export const openInvestigationsTab = () => */ export const openHistoryTab = () => cy.get(DOCUMENT_DETAILS_FLYOUT_HISTORY_TAB).should('be.visible').click(); + +/** + * Filter table under the Table tab in the alert details expandable flyout right section + */ +export const filterTableTabTable = (filterValue: string) => + cy.get(DOCUMENT_DETAILS_FLYOUT_BODY).within(() => { + cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_FILTER).type(filterValue); + }); + +/** + * Clear table filter under the Table tab in the alert details expandable flyout right section + */ +export const clearFilterTableTabTable = () => + cy.get(DOCUMENT_DETAILS_FLYOUT_BODY).within(() => { + cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_CLEAR_FILTER).click(); + }); + +/** + * Filter In action in the first table row under the Table tab in the alert details expandable flyout right section + */ +export const filterInTableTabTable = () => + cy.get(DOCUMENT_DETAILS_FLYOUT_BODY).within(() => { + cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_FILTER_IN).first().click(); + }); + +/** + * Filter Out action in the first table row under the Table tab in the alert details expandable flyout right section + */ +export const filterOutTableTabTable = () => + cy.get(DOCUMENT_DETAILS_FLYOUT_BODY).within(() => { + cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_FILTER_OUT).first().click(); + }); + +/** + * Add to timeline action in the first table row under the Table tab in the alert details expandable flyout right section + */ +export const addToTimelineTableTabTable = () => { + cy.get(DOCUMENT_DETAILS_FLYOUT_BODY).within(() => { + cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_MORE_ACTIONS).first().click(); + }); + cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_ADD_TO_TIMELINE).click(); +}; + +/** + * Show Copy to clipboard button in the first table row under the Table tab in the alert details expandable flyout right section + */ +export const copyToClipboardTableTabTable = () => { + cy.get(DOCUMENT_DETAILS_FLYOUT_BODY).within(() => { + cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_MORE_ACTIONS).first().click(); + }); +}; + +/** + * Clear filters in the alert page KQL bar + */ +export const clearFilters = () => + cy.get(DOCUMENT_DETAILS_FLYOUT_BODY).within(() => { + cy.get(DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_FILTER_OUT).first().click(); + }); diff --git a/x-pack/plugins/security_solution/public/flyout/right/content.tsx b/x-pack/plugins/security_solution/public/flyout/right/content.tsx index 4bf41518a0b2e..9dd8391d24d11 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/content.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/content.tsx @@ -8,6 +8,7 @@ import { EuiFlyoutBody } from '@elastic/eui'; import type { VFC } from 'react'; import React, { useMemo } from 'react'; +import { FLYOUT_BODY_TEST_ID } from './test_ids'; import type { RightPanelPaths } from '.'; import { tabs } from './tabs'; @@ -27,7 +28,7 @@ export const PanelContent: VFC = ({ selectedTabId }) => { return tabs.find((tab) => tab.id === selectedTabId)?.content; }, [selectedTabId]); - return {selectedTabContent}; + return {selectedTabContent}; }; PanelContent.displayName = 'PanelContent'; diff --git a/x-pack/plugins/security_solution/public/flyout/right/tabs/table_tab.stories.tsx b/x-pack/plugins/security_solution/public/flyout/right/tabs/table_tab.stories.tsx new file mode 100644 index 0000000000000..b0ff8800f8c1f --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/tabs/table_tab.stories.tsx @@ -0,0 +1,47 @@ +/* + * 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 { TableTab } from './table_tab'; + +export default { + component: TableTab, + title: 'Flyout/TableTab', +}; + +// 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 = () => { +// const contextValue = { +// eventId: 'some_id', +// browserFields: {}, +// dataFormattedForFieldBrowser: [], +// } as unknown as RightPanelContext; +// +// return ( +// +// +// +// ); +// }; + +export const Error: Story = () => { + const contextValue = { + eventId: null, + browserFields: {}, + dataFormattedForFieldBrowser: [], + } as unknown as RightPanelContext; + + return ( + + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/flyout/right/tabs/table_tab.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/tabs/table_tab.test.tsx new file mode 100644 index 0000000000000..16e0d84cd083a --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/tabs/table_tab.test.tsx @@ -0,0 +1,103 @@ +/* + * 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 { TABLE_TAB_ERROR_TEST_ID, TABLE_TAB_CONTENT_TEST_ID } from './test_ids'; +import { TableTab } from './table_tab'; +import { TestProviders } from '../../../common/mock'; + +const mockDispatch = jest.fn(); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); + +describe('', () => { + it('should render table component', () => { + const contextValue = { + eventId: 'some_Id', + browserFields: {}, + dataFormattedForFieldBrowser: [], + } as unknown as RightPanelContext; + + const { getByTestId } = render( + + + + + + ); + + expect(getByTestId(TABLE_TAB_CONTENT_TEST_ID)).toBeInTheDocument(); + }); + + it('should render error message on null browserFields', () => { + const contextValue = { + eventId: 'some_Id', + browserFields: null, + dataFormattedForFieldBrowser: [], + } as unknown as RightPanelContext; + + const { getByTestId, getByText } = render( + + + + ); + + expect(getByTestId(TABLE_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(); + }); + + it('should render error message on null dataFormattedForFieldBrowser', () => { + const contextValue = { + eventId: 'some_Id', + browserFields: {}, + dataFormattedForFieldBrowser: null, + } as unknown as RightPanelContext; + + const { getByTestId, getByText } = render( + + + + ); + + expect(getByTestId(TABLE_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(); + }); + + it('should render error message on null eventId', () => { + const contextValue = { + eventId: null, + browserFields: {}, + dataFormattedForFieldBrowser: [], + } as unknown as RightPanelContext; + + const { getByTestId, getByText } = render( + + + + ); + + expect(getByTestId(TABLE_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(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/right/tabs/table_tab.tsx b/x-pack/plugins/security_solution/public/flyout/right/tabs/table_tab.tsx index 77edb9ffc61f9..949679d27de44 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/tabs/table_tab.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/tabs/table_tab.tsx @@ -5,16 +5,44 @@ * 2.0. */ -import { EuiText } from '@elastic/eui'; import type { FC } from 'react'; import React, { memo } from 'react'; -import { TABLE_TAB_CONTENT_TEST_ID } from './test_ids'; +import { EuiEmptyPrompt } from '@elastic/eui'; +import { ERROR_MESSAGE, ERROR_TITLE } from './translations'; +import { TimelineTabs } from '../../../../common/types'; +import { EventFieldsBrowser } from '../../../common/components/event_details/event_fields_browser'; +import { useRightPanelContext } from '../context'; +import { TABLE_TAB_ERROR_TEST_ID } from './test_ids'; /** * Table view displayed in the document details expandable flyout right section */ export const TableTab: FC = memo(() => { - return {'Table tab'}; + const { browserFields, dataFormattedForFieldBrowser, eventId } = useRightPanelContext(); + + if (!browserFields || !eventId || !dataFormattedForFieldBrowser) { + return ( + {ERROR_TITLE}} + body={

{ERROR_MESSAGE}

} + data-test-subj={TABLE_TAB_ERROR_TEST_ID} + /> + ); + } + + return ( + + ); }); TableTab.displayName = 'TableTab'; diff --git a/x-pack/plugins/security_solution/public/flyout/right/tabs/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/right/tabs/test_ids.ts index c80371ba6b55a..858cb6762fa8c 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/tabs/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/right/tabs/test_ids.ts @@ -7,6 +7,7 @@ export const OVERVIEW_TAB_CONTENT_TEST_ID = 'securitySolutionDocumentDetailsFlyoutOverviewTabContent'; -export const TABLE_TAB_CONTENT_TEST_ID = 'securitySolutionDocumentDetailsFlyoutTableTabContent'; +export const TABLE_TAB_CONTENT_TEST_ID = 'event-fields-browser'; +export const TABLE_TAB_ERROR_TEST_ID = 'securitySolutionAlertDetailsFlyoutTableTabError'; export const JSON_TAB_CONTENT_TEST_ID = 'jsonView'; export const JSON_TAB_ERROR_TEST_ID = 'securitySolutionDocumentDetailsFlyoutJsonTabError'; diff --git a/x-pack/plugins/security_solution/public/flyout/right/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/right/test_ids.ts index 3e3f16c629d2c..2c66b30bcca6d 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/right/test_ids.ts @@ -5,6 +5,7 @@ * 2.0. */ +export const FLYOUT_BODY_TEST_ID = 'securitySolutionDocumentDetailsFlyoutBody'; export const OVERVIEW_TAB_TEST_ID = 'securitySolutionDocumentDetailsFlyoutOverviewTab'; export const TABLE_TAB_TEST_ID = 'securitySolutionDocumentDetailsFlyoutTableTab'; export const JSON_TAB_TEST_ID = 'securitySolutionDocumentDetailsFlyoutJsonTab';