From c55bb917fab509df7922ac727d4246514dbd7a59 Mon Sep 17 00:00:00 2001 From: Karl Godard Date: Wed, 23 Mar 2022 08:23:22 -0700 Subject: [PATCH 01/13] [Security team: AWP] Session view: Alert details tab (#127500) * alerts tab work. list view done * View mode toggle + group view implemented * tests written * clean up * addressed @opauloh comments * fixed weird bug due to importing assests from a test into its component * empty state added for alerts tab * react-query caching keys updated to include sessionEntityId * rule_registry added as a dependency in order to use AlertsClient in alerts_route.ts * fixed build/test errors due to merge. events route now orders by process.start then @timestamp * plumbing for the alert details tie in done. * removed rule_registry ecs mappings. kqualters PR will add this. * alerts index merge conflict fix Co-authored-by: mitodrummer --- .../plugins/session_view/common/constants.ts | 3 +- x-pack/plugins/session_view/kibana.json | 7 +- .../detail_panel_alert_actions/index.test.tsx | 88 ++++++ .../detail_panel_alert_actions/index.tsx | 105 ++++++++ .../detail_panel_alert_actions/styles.ts | 107 ++++++++ .../detail_panel_alert_group_item/index.tsx | 84 ++++++ .../detail_panel_alert_list_item/index.tsx | 137 ++++++++++ .../detail_panel_alert_list_item/styles.ts | 112 ++++++++ .../detail_panel_alert_tab/index.test.tsx | 251 ++++++++++++++++++ .../detail_panel_alert_tab/index.tsx | 146 ++++++++++ .../detail_panel_alert_tab/styles.ts | 43 +++ .../public/components/process_tree/hooks.ts | 13 + .../components/process_tree/index.test.tsx | 5 +- .../public/components/process_tree/index.tsx | 12 +- .../process_tree_alert/index.test.tsx | 10 +- .../components/process_tree_alert/index.tsx | 12 +- .../process_tree_alerts/index.test.tsx | 2 +- .../components/process_tree_alerts/index.tsx | 9 +- .../process_tree_node/index.test.tsx | 2 +- .../components/process_tree_node/index.tsx | 48 +++- .../public/components/session_view/hooks.ts | 35 ++- .../public/components/session_view/index.tsx | 34 ++- .../public/components/session_view/styles.ts | 11 + .../session_view_detail_panel/index.test.tsx | 48 +++- .../session_view_detail_panel/index.tsx | 71 +++-- x-pack/plugins/session_view/server/plugin.ts | 12 +- .../server/routes/alerts_route.test.ts | 133 ++++++++++ .../server/routes/alerts_route.ts | 66 +++++ .../session_view/server/routes/index.ts | 5 +- .../server/routes/process_events_route.ts | 28 +- x-pack/plugins/session_view/server/types.ts | 15 +- x-pack/plugins/session_view/tsconfig.json | 3 +- 32 files changed, 1555 insertions(+), 102 deletions(-) create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.test.tsx create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.tsx create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_alert_actions/styles.ts create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_alert_group_item/index.tsx create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.tsx create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/styles.ts create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.test.tsx create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx create mode 100644 x-pack/plugins/session_view/public/components/detail_panel_alert_tab/styles.ts create mode 100644 x-pack/plugins/session_view/server/routes/alerts_route.test.ts create mode 100644 x-pack/plugins/session_view/server/routes/alerts_route.ts diff --git a/x-pack/plugins/session_view/common/constants.ts b/x-pack/plugins/session_view/common/constants.ts index 42e1d33ab6dba..9e8e1ae0d5e04 100644 --- a/x-pack/plugins/session_view/common/constants.ts +++ b/x-pack/plugins/session_view/common/constants.ts @@ -6,10 +6,11 @@ */ export const PROCESS_EVENTS_ROUTE = '/internal/session_view/process_events_route'; +export const ALERTS_ROUTE = '/internal/session_view/alerts_route'; export const ALERT_STATUS_ROUTE = '/internal/session_view/alert_status_route'; export const SESSION_ENTRY_LEADERS_ROUTE = '/internal/session_view/session_entry_leaders_route'; export const PROCESS_EVENTS_INDEX = 'logs-endpoint.events.process-default'; -export const ALERTS_INDEX = '.siem-signals-default'; +export const ALERTS_INDEX = '.alerts-security.alerts-default'; // TODO: changes to remove this and use AlertsClient instead to get indices. export const ENTRY_SESSION_ENTITY_ID_PROPERTY = 'process.entry_leader.entity_id'; export const ALERT_UUID_PROPERTY = 'kibana.alert.uuid'; export const KIBANA_DATE_FORMAT = 'MMM DD, YYYY @ hh:mm:ss.SSS'; diff --git a/x-pack/plugins/session_view/kibana.json b/x-pack/plugins/session_view/kibana.json index ff9d849016c55..4807315569d34 100644 --- a/x-pack/plugins/session_view/kibana.json +++ b/x-pack/plugins/session_view/kibana.json @@ -1,6 +1,6 @@ { "id": "sessionView", - "version": "8.0.0", + "version": "1.0.0", "kibanaVersion": "kibana", "owner": { "name": "Security Team", @@ -8,10 +8,11 @@ }, "requiredPlugins": [ "data", - "timelines" + "timelines", + "ruleRegistry" ], "requiredBundles": [ - "kibanaReact", + "kibanaReact", "esUiShared" ], "server": true, diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.test.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.test.tsx new file mode 100644 index 0000000000000..1d0c9d0227699 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.test.tsx @@ -0,0 +1,88 @@ +/* + * 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 { AppContextTestRender, createAppRootMockRenderer } from '../../test'; +import { + DetailPanelAlertActions, + BUTTON_TEST_ID, + SHOW_DETAILS_TEST_ID, + JUMP_TO_PROCESS_TEST_ID, +} from './index'; +import { mockAlerts } from '../../../common/mocks/constants/session_view_process.mock'; +import userEvent from '@testing-library/user-event'; +import { ProcessImpl } from '../process_tree/hooks'; + +describe('DetailPanelAlertActions component', () => { + let render: () => ReturnType; + let renderResult: ReturnType; + let mockedContext: AppContextTestRender; + let mockShowAlertDetails = jest.fn((uuid) => uuid); + let mockOnProcessSelected = jest.fn((process) => process); + + beforeEach(() => { + mockedContext = createAppRootMockRenderer(); + mockShowAlertDetails = jest.fn((uuid) => uuid); + mockOnProcessSelected = jest.fn((process) => process); + }); + + describe('When DetailPanelAlertActions is mounted', () => { + it('renders a popover when button is clicked', async () => { + const mockEvent = mockAlerts[0]; + + renderResult = mockedContext.render( + + ); + + userEvent.click(renderResult.getByTestId(BUTTON_TEST_ID)); + expect(renderResult.queryByTestId(SHOW_DETAILS_TEST_ID)).toBeTruthy(); + expect(renderResult.queryByTestId(JUMP_TO_PROCESS_TEST_ID)).toBeTruthy(); + expect(mockShowAlertDetails.mock.calls.length).toBe(0); + expect(mockOnProcessSelected.mock.calls.length).toBe(0); + }); + + it('calls alert flyout callback when View details clicked', async () => { + const mockEvent = mockAlerts[0]; + + renderResult = mockedContext.render( + + ); + + userEvent.click(renderResult.getByTestId(BUTTON_TEST_ID)); + userEvent.click(renderResult.getByTestId(SHOW_DETAILS_TEST_ID)); + expect(mockShowAlertDetails.mock.calls.length).toBe(1); + expect(mockShowAlertDetails.mock.results[0].value).toBe(mockEvent.kibana?.alert.uuid); + expect(mockOnProcessSelected.mock.calls.length).toBe(0); + }); + + it('calls onProcessSelected when Jump to process clicked', async () => { + const mockEvent = mockAlerts[0]; + + renderResult = mockedContext.render( + + ); + + userEvent.click(renderResult.getByTestId(BUTTON_TEST_ID)); + userEvent.click(renderResult.getByTestId(JUMP_TO_PROCESS_TEST_ID)); + expect(mockOnProcessSelected.mock.calls.length).toBe(1); + expect(mockOnProcessSelected.mock.results[0].value).toBeInstanceOf(ProcessImpl); + expect(mockShowAlertDetails.mock.calls.length).toBe(0); + }); + }); +}); diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.tsx new file mode 100644 index 0000000000000..4c7e3fdfaa961 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/index.tsx @@ -0,0 +1,105 @@ +/* + * 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, { useState, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiPopover, EuiContextMenuPanel, EuiButtonIcon, EuiContextMenuItem } from '@elastic/eui'; +import { Process, ProcessEvent } from '../../../common/types/process_tree'; +import { ProcessImpl } from '../process_tree/hooks'; + +export const BUTTON_TEST_ID = 'sessionView:detailPanelAlertActionsBtn'; +export const SHOW_DETAILS_TEST_ID = 'sessionView:detailPanelAlertActionShowDetails'; +export const JUMP_TO_PROCESS_TEST_ID = 'sessionView:detailPanelAlertActionJumpToProcess'; + +interface DetailPanelAlertActionsDeps { + event: ProcessEvent; + onShowAlertDetails: (alertId: string) => void; + onProcessSelected: (process: Process) => void; +} + +/** + * Detail panel alert context menu actions + */ +export const DetailPanelAlertActions = ({ + event, + onShowAlertDetails, + onProcessSelected, +}: DetailPanelAlertActionsDeps) => { + const [isPopoverOpen, setPopover] = useState(false); + + const onClosePopover = useCallback(() => { + setPopover(false); + }, []); + + const onToggleMenu = useCallback(() => { + setPopover(!isPopoverOpen); + }, [isPopoverOpen]); + + const onJumpToAlert = useCallback(() => { + const process = new ProcessImpl(event.process.entity_id); + process.addEvent(event); + + onProcessSelected(process); + setPopover(false); + }, [event, onProcessSelected]); + + const onShowDetails = useCallback(() => { + if (event.kibana) { + onShowAlertDetails(event.kibana.alert.uuid); + setPopover(false); + } + }, [event, onShowAlertDetails]); + + if (!event.kibana) { + return null; + } + + const { uuid } = event.kibana.alert; + + const menuItems = [ + + + , + + + , + ]; + + return ( + + } + isOpen={isPopoverOpen} + closePopover={onClosePopover} + panelPaddingSize="none" + anchorPosition="leftCenter" + > + + + ); +}; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/styles.ts b/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/styles.ts new file mode 100644 index 0000000000000..14d0be374b5d1 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_actions/styles.ts @@ -0,0 +1,107 @@ +/* + * 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 { useMemo } from 'react'; +import { useEuiTheme, transparentize } from '@elastic/eui'; +import { CSSObject, css } from '@emotion/react'; + +interface StylesDeps { + minimal?: boolean; + isInvestigated?: boolean; +} + +export const useStyles = ({ minimal = false, isInvestigated = false }: StylesDeps) => { + const { euiTheme } = useEuiTheme(); + + const cached = useMemo(() => { + const { colors, font, size, border } = euiTheme; + + const dangerBorder = transparentize(colors.danger, 0.2); + const dangerBackground = transparentize(colors.danger, 0.08); + const borderThickness = border.width.thin; + const mediumPadding = size.m; + + let alertTitleColor = colors.text; + let borderColor = colors.lightShade; + + if (isInvestigated) { + alertTitleColor = colors.primaryText; + borderColor = dangerBorder; + } + + const alertItem = css` + border: ${borderThickness} solid ${borderColor}; + padding: ${mediumPadding}; + border-radius: ${border.radius.medium}; + + margin: 0 ${mediumPadding} ${mediumPadding} ${mediumPadding}; + background-color: ${colors.emptyShade}; + + & .euiAccordion__buttonContent { + width: 100%; + } + + & .euiAccordion__button { + min-width: 0; + width: calc(100% - ${size.l}); + } + + & .euiAccordion__childWrapper { + overflow: visible; + } + `; + + const alertTitle: CSSObject = { + display: minimal ? 'none' : 'initial', + color: alertTitleColor, + fontWeight: font.weight.semiBold, + textOverflow: 'ellipsis', + overflow: 'hidden', + whiteSpace: 'nowrap', + }; + + const alertIcon: CSSObject = { + marginRight: size.s, + }; + + const alertAccordionButton: CSSObject = { + width: `calc(100% - ${size.l})`, + minWidth: 0, + }; + + const processPanel: CSSObject = { + border: `${borderThickness} solid ${colors.lightShade}`, + fontFamily: font.familyCode, + marginTop: mediumPadding, + padding: `${size.xs} ${size.s}`, + }; + + const investigatedLabel: CSSObject = { + position: 'relative', + zIndex: 1, + bottom: `-${mediumPadding}`, + left: `-${mediumPadding}`, + width: `calc(100% + ${mediumPadding} * 2)`, + borderTop: `${borderThickness} solid ${dangerBorder}`, + borderBottomLeftRadius: border.radius.medium, + borderBottomRightRadius: border.radius.medium, + backgroundColor: dangerBackground, + textAlign: 'center', + }; + + return { + alertItem, + alertTitle, + alertIcon, + alertAccordionButton, + processPanel, + investigatedLabel, + }; + }, [euiTheme, isInvestigated, minimal]); + + return cached; +}; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_group_item/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_group_item/index.tsx new file mode 100644 index 0000000000000..daa472cd6e5b4 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_group_item/index.tsx @@ -0,0 +1,84 @@ +/* + * 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, { useMemo } from 'react'; +import { EuiIcon, EuiText, EuiAccordion, EuiNotificationBadge } from '@elastic/eui'; +import { Process, ProcessEvent } from '../../../common/types/process_tree'; +import { useStyles } from '../detail_panel_alert_list_item/styles'; +import { DetailPanelAlertListItem } from '../detail_panel_alert_list_item'; +import { ALERT_COUNT_THRESHOLD } from '../../../common/constants'; + +export const ALERT_GROUP_ITEM_TEST_ID = 'sessionView:detailPanelAlertGroupItem'; +export const ALERT_GROUP_ITEM_COUNT_TEST_ID = 'sessionView:detailPanelAlertGroupCount'; +export const ALERT_GROUP_ITEM_TITLE_TEST_ID = 'sessionView:detailPanelAlertGroupTitle'; + +interface DetailPanelAlertsGroupItemDeps { + alerts: ProcessEvent[]; + onProcessSelected: (process: Process) => void; + onShowAlertDetails: (alertId: string) => void; +} + +/** + * Detail panel description list item. + */ +export const DetailPanelAlertGroupItem = ({ + alerts, + onProcessSelected, + onShowAlertDetails, +}: DetailPanelAlertsGroupItemDeps) => { + const styles = useStyles(); + + const alertsCount = useMemo(() => { + return alerts.length >= ALERT_COUNT_THRESHOLD ? ALERT_COUNT_THRESHOLD + '+' : alerts.length; + }, [alerts]); + + if (!alerts[0].kibana) { + return null; + } + + const { rule } = alerts[0].kibana.alert; + + return ( + +

+ + {rule.name} +

+ + } + css={styles.alertItem} + extraAction={ + + {alertsCount} + + } + > + {alerts.map((event) => { + const key = 'minimal_' + event.kibana?.alert.uuid; + + return ( + + ); + })} +
+ ); +}; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.tsx new file mode 100644 index 0000000000000..516d04539432e --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/index.tsx @@ -0,0 +1,137 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n-react'; +import { + EuiFlexItem, + EuiFlexGroup, + EuiSpacer, + EuiIcon, + EuiText, + EuiAccordion, + EuiPanel, + EuiHorizontalRule, +} from '@elastic/eui'; +import { Process, ProcessEvent } from '../../../common/types/process_tree'; +import { useStyles } from './styles'; +import { DetailPanelAlertActions } from '../detail_panel_alert_actions'; + +export const ALERT_LIST_ITEM_TEST_ID = 'sessionView:detailPanelAlertListItem'; +export const ALERT_LIST_ITEM_ARGS_TEST_ID = 'sessionView:detailPanelAlertListItemArgs'; +export const ALERT_LIST_ITEM_TIMESTAMP_TEST_ID = 'sessionView:detailPanelAlertListItemTimestamp'; + +interface DetailPanelAlertsListItemDeps { + event: ProcessEvent; + onShowAlertDetails: (alertId: string) => void; + onProcessSelected: (process: Process) => void; + isInvestigated?: boolean; + minimal?: boolean; +} + +/** + * Detail panel description list item. + */ +export const DetailPanelAlertListItem = ({ + event, + onProcessSelected, + onShowAlertDetails, + isInvestigated, + minimal, +}: DetailPanelAlertsListItemDeps) => { + const styles = useStyles(minimal, isInvestigated); + + if (!event.kibana) { + return null; + } + + const timestamp = event['@timestamp']; + const { uuid, name } = event.kibana.alert.rule; + const { args } = event.process; + + const forceState = !isInvestigated ? 'open' : undefined; + + return minimal ? ( +
+ + + + + {timestamp} + + + + + + + + {args.join(' ')} + + +
+ ) : ( + +

+ + {name} +

+ + } + initialIsOpen={true} + forceState={forceState} + css={styles.alertItem} + extraAction={ + + } + > + + + {timestamp} + + + + {args.join(' ')} + + + {isInvestigated && ( +
+ + + +
+ )} +
+ ); +}; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/styles.ts b/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/styles.ts new file mode 100644 index 0000000000000..7672bb942ff32 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_list_item/styles.ts @@ -0,0 +1,112 @@ +/* + * 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 { useMemo } from 'react'; +import { useEuiTheme, transparentize } from '@elastic/eui'; +import { CSSObject, css } from '@emotion/react'; + +export const useStyles = (minimal = false, isInvestigated = false) => { + const { euiTheme } = useEuiTheme(); + + const cached = useMemo(() => { + const { colors, font, size, border } = euiTheme; + + const dangerBorder = transparentize(colors.danger, 0.2); + const dangerBackground = transparentize(colors.danger, 0.08); + const borderThickness = border.width.thin; + const mediumPadding = size.m; + + let alertTitleColor = colors.text; + let borderColor = colors.lightShade; + + if (isInvestigated) { + alertTitleColor = colors.primaryText; + borderColor = dangerBorder; + } + + const alertItem = css` + border: ${borderThickness} solid ${borderColor}; + padding: ${mediumPadding}; + border-radius: ${border.radius.medium}; + + margin: 0 ${mediumPadding} ${mediumPadding} ${mediumPadding}; + background-color: ${colors.emptyShade}; + + & .euiAccordion__buttonContent { + width: 100%; + } + + & .euiAccordion__button { + min-width: 0; + width: calc(100% - ${size.l}); + } + + & .euiAccordion__childWrapper { + overflow: visible; + } + `; + + const alertTitle: CSSObject = { + display: minimal ? 'none' : 'initial', + color: alertTitleColor, + fontWeight: font.weight.semiBold, + textOverflow: 'ellipsis', + overflow: 'hidden', + whiteSpace: 'nowrap', + }; + + const alertIcon: CSSObject = { + marginRight: size.s, + }; + + const alertAccordionButton: CSSObject = { + width: `calc(100% - ${size.l})`, + minWidth: 0, + }; + + const processPanel: CSSObject = { + border: `${borderThickness} solid ${colors.lightShade}`, + fontFamily: font.familyCode, + marginTop: minimal ? size.s : size.m, + padding: `${size.xs} ${size.s}`, + }; + + const investigatedLabel: CSSObject = { + position: 'relative', + zIndex: 1, + bottom: `-${mediumPadding}`, + left: `-${mediumPadding}`, + width: `calc(100% + ${mediumPadding} * 2)`, + borderTop: `${borderThickness} solid ${dangerBorder}`, + borderBottomLeftRadius: border.radius.medium, + borderBottomRightRadius: border.radius.medium, + backgroundColor: dangerBackground, + textAlign: 'center', + }; + + const minimalContextMenu: CSSObject = { + float: 'right', + }; + + const minimalHR: CSSObject = { + marginBottom: 0, + }; + + return { + alertItem, + alertTitle, + alertIcon, + alertAccordionButton, + processPanel, + investigatedLabel, + minimalContextMenu, + minimalHR, + }; + }, [euiTheme, isInvestigated, minimal]); + + return cached; +}; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.test.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.test.tsx new file mode 100644 index 0000000000000..a915f8e285ad1 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.test.tsx @@ -0,0 +1,251 @@ +/* + * 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 { AppContextTestRender, createAppRootMockRenderer } from '../../test'; +import { DetailPanelAlertTab } from './index'; +import { mockAlerts } from '../../../common/mocks/constants/session_view_process.mock'; +import { fireEvent } from '@testing-library/dom'; +import { + INVESTIGATED_ALERT_TEST_ID, + VIEW_MODE_TOGGLE, + ALERTS_TAB_EMPTY_STATE_TEST_ID, +} from './index'; +import { + ALERT_LIST_ITEM_TEST_ID, + ALERT_LIST_ITEM_ARGS_TEST_ID, + ALERT_LIST_ITEM_TIMESTAMP_TEST_ID, +} from '../detail_panel_alert_list_item/index'; +import { + ALERT_GROUP_ITEM_TEST_ID, + ALERT_GROUP_ITEM_COUNT_TEST_ID, + ALERT_GROUP_ITEM_TITLE_TEST_ID, +} from '../detail_panel_alert_group_item/index'; + +const ACCORDION_BUTTON_CLASS = '.euiAccordion__button'; +const VIEW_MODE_GROUP = 'groupView'; +const ARIA_EXPANDED_ATTR = 'aria-expanded'; + +describe('DetailPanelAlertTab component', () => { + let render: () => ReturnType; + let renderResult: ReturnType; + let mockedContext: AppContextTestRender; + let mockOnProcessSelected = jest.fn((process) => process); + let mockShowAlertDetails = jest.fn((alertId) => alertId); + + beforeEach(() => { + mockedContext = createAppRootMockRenderer(); + mockOnProcessSelected = jest.fn((process) => process); + mockShowAlertDetails = jest.fn((alertId) => alertId); + }); + + describe('When DetailPanelAlertTab is mounted', () => { + it('renders a list of alerts for the session (defaulting to list view mode)', async () => { + renderResult = mockedContext.render( + + ); + + expect(renderResult.queryAllByTestId(ALERT_LIST_ITEM_TEST_ID).length).toBe(mockAlerts.length); + expect(renderResult.queryByTestId(ALERT_GROUP_ITEM_TEST_ID)).toBeFalsy(); + expect(renderResult.queryByTestId(INVESTIGATED_ALERT_TEST_ID)).toBeFalsy(); + expect( + renderResult + .queryByTestId(VIEW_MODE_TOGGLE) + ?.querySelector('.euiButtonGroupButton-isSelected')?.textContent + ).toBe('List view'); + }); + + it('renders a list of alerts grouped by rule when group-view clicked', async () => { + renderResult = mockedContext.render( + + ); + + fireEvent.click(renderResult.getByTestId(VIEW_MODE_GROUP)); + + expect(renderResult.queryAllByTestId(ALERT_LIST_ITEM_TEST_ID).length).toBe(mockAlerts.length); + expect(renderResult.queryByTestId(ALERT_GROUP_ITEM_TEST_ID)).toBeTruthy(); + expect(renderResult.queryByTestId(INVESTIGATED_ALERT_TEST_ID)).toBeFalsy(); + expect( + renderResult + .queryByTestId(VIEW_MODE_TOGGLE) + ?.querySelector('.euiButtonGroupButton-isSelected')?.textContent + ).toBe('Group view'); + }); + + it('renders a sticky investigated alert (outside of main list) if one is set', async () => { + renderResult = mockedContext.render( + + ); + + expect(renderResult.queryByTestId(INVESTIGATED_ALERT_TEST_ID)).toBeTruthy(); + + fireEvent.click(renderResult.getByTestId(VIEW_MODE_GROUP)); + + expect(renderResult.queryByTestId(INVESTIGATED_ALERT_TEST_ID)).toBeTruthy(); + }); + + it('investigated alert should be collapsible', async () => { + renderResult = mockedContext.render( + + ); + + expect( + renderResult + .queryByTestId(INVESTIGATED_ALERT_TEST_ID) + ?.querySelector(ACCORDION_BUTTON_CLASS) + ?.attributes.getNamedItem(ARIA_EXPANDED_ATTR)?.value + ).toBe('true'); + + const expandButton = renderResult + .queryByTestId(INVESTIGATED_ALERT_TEST_ID) + ?.querySelector(ACCORDION_BUTTON_CLASS); + + if (expandButton) { + fireEvent.click(expandButton); + } + + expect( + renderResult + .queryByTestId(INVESTIGATED_ALERT_TEST_ID) + ?.querySelector(ACCORDION_BUTTON_CLASS) + ?.attributes.getNamedItem(ARIA_EXPANDED_ATTR)?.value + ).toBe('false'); + }); + + it('non investigated alert should NOT be collapsible', async () => { + renderResult = mockedContext.render( + + ); + + expect( + renderResult + .queryAllByTestId(ALERT_LIST_ITEM_TEST_ID)[0] + ?.querySelector(ACCORDION_BUTTON_CLASS) + ?.attributes.getNamedItem(ARIA_EXPANDED_ATTR)?.value + ).toBe('true'); + + const expandButton = renderResult + .queryAllByTestId(ALERT_LIST_ITEM_TEST_ID)[0] + ?.querySelector(ACCORDION_BUTTON_CLASS); + + if (expandButton) { + fireEvent.click(expandButton); + } + + expect( + renderResult + .queryAllByTestId(ALERT_LIST_ITEM_TEST_ID)[0] + ?.querySelector(ACCORDION_BUTTON_CLASS) + ?.attributes.getNamedItem(ARIA_EXPANDED_ATTR)?.value + ).toBe('true'); + }); + + it('grouped alerts should be expandable/collapsible (default to collapsed)', async () => { + renderResult = mockedContext.render( + + ); + + fireEvent.click(renderResult.getByTestId(VIEW_MODE_GROUP)); + + expect( + renderResult + .queryAllByTestId(ALERT_GROUP_ITEM_TEST_ID)[0] + ?.querySelector(ACCORDION_BUTTON_CLASS) + ?.attributes.getNamedItem(ARIA_EXPANDED_ATTR)?.value + ).toBe('false'); + + const expandButton = renderResult + .queryAllByTestId(ALERT_GROUP_ITEM_TEST_ID)[0] + ?.querySelector(ACCORDION_BUTTON_CLASS); + + if (expandButton) { + fireEvent.click(expandButton); + } + + expect( + renderResult + .queryAllByTestId(ALERT_GROUP_ITEM_TEST_ID)[0] + ?.querySelector(ACCORDION_BUTTON_CLASS) + ?.attributes.getNamedItem(ARIA_EXPANDED_ATTR)?.value + ).toBe('true'); + }); + + it('each alert list item should show a timestamp and process arguments', async () => { + renderResult = mockedContext.render( + + ); + + expect(renderResult.queryAllByTestId(ALERT_LIST_ITEM_TIMESTAMP_TEST_ID)[0]).toHaveTextContent( + mockAlerts[0]['@timestamp'] + ); + + expect(renderResult.queryAllByTestId(ALERT_LIST_ITEM_ARGS_TEST_ID)[0]).toHaveTextContent( + mockAlerts[0].process.args.join(' ') + ); + }); + + it('each alert group should show a rule title and alert count', async () => { + renderResult = mockedContext.render( + + ); + + fireEvent.click(renderResult.getByTestId(VIEW_MODE_GROUP)); + + expect(renderResult.queryByTestId(ALERT_GROUP_ITEM_COUNT_TEST_ID)).toHaveTextContent('2'); + expect(renderResult.queryByTestId(ALERT_GROUP_ITEM_TITLE_TEST_ID)).toHaveTextContent( + mockAlerts[0].kibana?.alert.rule.name || '' + ); + }); + + it('renders an empty state when there are no alerts', async () => { + renderResult = mockedContext.render( + + ); + + expect(renderResult.queryByTestId(ALERTS_TAB_EMPTY_STATE_TEST_ID)).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx new file mode 100644 index 0000000000000..7fa47f4f5daf7 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/index.tsx @@ -0,0 +1,146 @@ +/* + * 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, { useState, useMemo } from 'react'; +import { EuiEmptyPrompt, EuiButtonGroup, EuiHorizontalRule } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { groupBy } from 'lodash'; +import { ProcessEvent, Process } from '../../../common/types/process_tree'; +import { useStyles } from './styles'; +import { DetailPanelAlertListItem } from '../detail_panel_alert_list_item'; +import { DetailPanelAlertGroupItem } from '../detail_panel_alert_group_item'; + +export const ALERTS_TAB_EMPTY_STATE_TEST_ID = 'sessionView:detailPanelAlertsEmptyState'; +export const INVESTIGATED_ALERT_TEST_ID = 'sessionView:detailPanelInvestigatedAlert'; +export const VIEW_MODE_TOGGLE = 'sessionView:detailPanelAlertsViewMode'; + +interface DetailPanelAlertTabDeps { + alerts: ProcessEvent[]; + onProcessSelected: (process: Process) => void; + onShowAlertDetails: (alertId: string) => void; + investigatedAlert?: ProcessEvent; +} + +const VIEW_MODE_LIST = 'listView'; +const VIEW_MODE_GROUP = 'groupView'; + +/** + * Host Panel of session view detail panel. + */ +export const DetailPanelAlertTab = ({ + alerts, + onProcessSelected, + onShowAlertDetails, + investigatedAlert, +}: DetailPanelAlertTabDeps) => { + const styles = useStyles(); + const [viewMode, setViewMode] = useState(VIEW_MODE_LIST); + const viewModes = [ + { + id: VIEW_MODE_LIST, + label: i18n.translate('xpack.sessionView.alertDetailsTab.listView', { + defaultMessage: 'List view', + }), + }, + { + id: VIEW_MODE_GROUP, + label: i18n.translate('xpack.sessionView.alertDetailsTab.groupView', { + defaultMessage: 'Group view', + }), + }, + ]; + + const filteredAlerts = useMemo(() => { + return alerts.filter((event) => { + const isInvestigatedAlert = + event.kibana?.alert.uuid === investigatedAlert?.kibana?.alert.uuid; + return !isInvestigatedAlert; + }); + }, [investigatedAlert, alerts]); + + const groupedAlerts = useMemo(() => { + return groupBy(filteredAlerts, (event) => event.kibana?.alert.rule.uuid); + }, [filteredAlerts]); + + if (alerts.length === 0) { + return ( + + + + } + body={ +

+ +

+ } + /> + ); + } + + return ( +
+ + {investigatedAlert && ( +
+ + +
+ )} + + {viewMode === VIEW_MODE_LIST + ? filteredAlerts.map((event) => { + const key = event.kibana?.alert.uuid; + + return ( + + ); + }) + : Object.keys(groupedAlerts).map((ruleId: string) => { + const alertsByRule = groupedAlerts[ruleId]; + + return ( + + ); + })} +
+ ); +}; diff --git a/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/styles.ts b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/styles.ts new file mode 100644 index 0000000000000..a906744cdafb2 --- /dev/null +++ b/x-pack/plugins/session_view/public/components/detail_panel_alert_tab/styles.ts @@ -0,0 +1,43 @@ +/* + * 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 { useMemo } from 'react'; +import { useEuiTheme } from '@elastic/eui'; +import { CSSObject } from '@emotion/react'; + +export const useStyles = () => { + const { euiTheme } = useEuiTheme(); + + const cached = useMemo(() => { + const { colors, size } = euiTheme; + + const container: CSSObject = { + position: 'relative', + }; + + const stickyItem: CSSObject = { + position: 'sticky', + top: 0, + zIndex: 1, + backgroundColor: colors.emptyShade, + paddingTop: size.base, + }; + + const viewMode: CSSObject = { + margin: size.base, + marginBottom: 0, + }; + + return { + container, + stickyItem, + viewMode, + }; + }, [euiTheme]); + + return cached; +}; diff --git a/x-pack/plugins/session_view/public/components/process_tree/hooks.ts b/x-pack/plugins/session_view/public/components/process_tree/hooks.ts index fb00344d5e280..2b7f78e88fafb 100644 --- a/x-pack/plugins/session_view/public/components/process_tree/hooks.ts +++ b/x-pack/plugins/session_view/public/components/process_tree/hooks.ts @@ -21,12 +21,14 @@ import { processNewEvents, searchProcessTree, autoExpandProcessTree, + updateProcessMap, } from './helpers'; import { sortProcesses } from '../../../common/utils/sort_processes'; interface UseProcessTreeDeps { sessionEntityId: string; data: ProcessEventsPage[]; + alerts: ProcessEvent[]; searchQuery?: string; updatedAlertsStatus: AlertStatusEventEntityIdMap; } @@ -196,6 +198,7 @@ export class ProcessImpl implements Process { export const useProcessTree = ({ sessionEntityId, data, + alerts, searchQuery, updatedAlertsStatus, }: UseProcessTreeDeps) => { @@ -221,6 +224,7 @@ export const useProcessTree = ({ const [processMap, setProcessMap] = useState(initializedProcessMap); const [processedPages, setProcessedPages] = useState([]); + const [alertsProcessed, setAlertsProcessed] = useState(false); const [searchResults, setSearchResults] = useState([]); const [orphans, setOrphans] = useState([]); @@ -257,6 +261,15 @@ export const useProcessTree = ({ } }, [data, processMap, orphans, processedPages, sessionEntityId]); + useEffect(() => { + // currently we are loading a single page of alerts, with no pagination + // so we only need to add these alert events to processMap once. + if (!alertsProcessed) { + updateProcessMap(processMap, alerts); + setAlertsProcessed(true); + } + }, [processMap, alerts, alertsProcessed]); + useEffect(() => { setSearchResults(searchProcessTree(processMap, searchQuery)); autoExpandProcessTree(processMap); diff --git a/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx b/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx index 9fa7900d04b0d..3c0b9c5d0d4d9 100644 --- a/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree/index.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { mockData } from '../../../common/mocks/constants/session_view_process.mock'; +import { mockData, mockAlerts } from '../../../common/mocks/constants/session_view_process.mock'; import { Process } from '../../../common/types/process_tree'; import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; import { ProcessImpl } from './hooks'; @@ -21,6 +21,7 @@ describe('ProcessTree component', () => { const props: ProcessTreeDeps = { sessionEntityId: sessionLeader.process.entity_id, data: mockData, + alerts: mockAlerts, isFetching: false, fetchNextPage: jest.fn(), hasNextPage: false, @@ -28,7 +29,7 @@ describe('ProcessTree component', () => { hasPreviousPage: false, onProcessSelected: jest.fn(), updatedAlertsStatus: {}, - handleOnAlertDetailsClosed: jest.fn(), + onShowAlertDetails: jest.fn(), }; beforeEach(() => { diff --git a/x-pack/plugins/session_view/public/components/process_tree/index.tsx b/x-pack/plugins/session_view/public/components/process_tree/index.tsx index 4b489797c7e26..1e10e58d1cca0 100644 --- a/x-pack/plugins/session_view/public/components/process_tree/index.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree/index.tsx @@ -26,6 +26,7 @@ export interface ProcessTreeDeps { sessionEntityId: string; data: ProcessEventsPage[]; + alerts: ProcessEvent[]; jumpToEvent?: ProcessEvent; isFetching: boolean; @@ -44,8 +45,7 @@ export interface ProcessTreeDeps { // a map for alerts with updated status and process.entity_id updatedAlertsStatus: AlertStatusEventEntityIdMap; - loadAlertDetails?: (alertUuid: string, handleOnAlertDetailsClosed: () => void) => void; - handleOnAlertDetailsClosed: (alertUuid: string) => void; + onShowAlertDetails: (alertUuid: string) => void; timeStampOn?: boolean; verboseModeOn?: boolean; } @@ -53,6 +53,7 @@ export interface ProcessTreeDeps { export const ProcessTree = ({ sessionEntityId, data, + alerts, jumpToEvent, isFetching, hasNextPage, @@ -64,8 +65,7 @@ export const ProcessTree = ({ onProcessSelected, setSearchResults, updatedAlertsStatus, - loadAlertDetails, - handleOnAlertDetailsClosed, + onShowAlertDetails, timeStampOn, verboseModeOn, }: ProcessTreeDeps) => { @@ -76,6 +76,7 @@ export const ProcessTree = ({ const { sessionLeader, processMap, searchResults } = useProcessTree({ sessionEntityId, data, + alerts, searchQuery, updatedAlertsStatus, }); @@ -203,8 +204,7 @@ export const ProcessTree = ({ selectedProcessId={selectedProcess?.id} scrollerRef={scrollerRef} onChangeJumpToEventVisibility={onChangeJumpToEventVisibility} - loadAlertDetails={loadAlertDetails} - handleOnAlertDetailsClosed={handleOnAlertDetailsClosed} + onShowAlertDetails={onShowAlertDetails} timeStampOn={timeStampOn} verboseModeOn={verboseModeOn} /> diff --git a/x-pack/plugins/session_view/public/components/process_tree_alert/index.test.tsx b/x-pack/plugins/session_view/public/components/process_tree_alert/index.test.tsx index 2a56a0ae2be67..c1b0c807528ec 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_alert/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_alert/index.test.tsx @@ -26,7 +26,7 @@ describe('ProcessTreeAlerts component', () => { isSelected: false, onClick: jest.fn(), selectAlert: jest.fn(), - handleOnAlertDetailsClosed: jest.fn(), + onShowAlertDetails: jest.fn(), }; beforeEach(() => { @@ -61,16 +61,16 @@ describe('ProcessTreeAlerts component', () => { expect(selectAlert).toHaveBeenCalledTimes(1); }); - it('should execute loadAlertDetails callback when clicking on expand button', async () => { - const loadAlertDetails = jest.fn(); + it('should execute onShowAlertDetails callback when clicking on expand button', async () => { + const onShowAlertDetails = jest.fn(); renderResult = mockedContext.render( - + ); const expandButton = renderResult.queryByTestId(EXPAND_BUTTON_TEST_ID); expect(expandButton).toBeTruthy(); expandButton?.click(); - expect(loadAlertDetails).toHaveBeenCalledTimes(1); + expect(onShowAlertDetails).toHaveBeenCalledTimes(1); }); }); }); diff --git a/x-pack/plugins/session_view/public/components/process_tree_alert/index.tsx b/x-pack/plugins/session_view/public/components/process_tree_alert/index.tsx index 5ec1c4a7693c3..30892d02c5428 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_alert/index.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_alert/index.tsx @@ -17,8 +17,7 @@ export interface ProcessTreeAlertDeps { isSelected: boolean; onClick: (alert: ProcessEventAlert | null) => void; selectAlert: (alertUuid: string) => void; - loadAlertDetails?: (alertUuid: string, handleOnAlertDetailsClosed: () => void) => void; - handleOnAlertDetailsClosed: (alertUuid: string, status?: string) => void; + onShowAlertDetails: (alertUuid: string) => void; } export const ProcessTreeAlert = ({ @@ -27,8 +26,7 @@ export const ProcessTreeAlert = ({ isSelected, onClick, selectAlert, - loadAlertDetails, - handleOnAlertDetailsClosed, + onShowAlertDetails, }: ProcessTreeAlertDeps) => { const styles = useStyles({ isInvestigated, isSelected }); @@ -41,10 +39,10 @@ export const ProcessTreeAlert = ({ }, [isInvestigated, uuid, selectAlert]); const handleExpandClick = useCallback(() => { - if (loadAlertDetails && uuid) { - loadAlertDetails(uuid, () => handleOnAlertDetailsClosed(uuid)); + if (uuid) { + onShowAlertDetails(uuid); } - }, [handleOnAlertDetailsClosed, loadAlertDetails, uuid]); + }, [onShowAlertDetails, uuid]); const handleClick = useCallback(() => { if (alert.kibana?.alert) { diff --git a/x-pack/plugins/session_view/public/components/process_tree_alerts/index.test.tsx b/x-pack/plugins/session_view/public/components/process_tree_alerts/index.test.tsx index 2333c71d36a51..ee6866f6a8a60 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_alerts/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_alerts/index.test.tsx @@ -17,7 +17,7 @@ describe('ProcessTreeAlerts component', () => { const props: ProcessTreeAlertsDeps = { alerts: mockAlerts, onAlertSelected: jest.fn(), - handleOnAlertDetailsClosed: jest.fn(), + onShowAlertDetails: jest.fn(), }; beforeEach(() => { diff --git a/x-pack/plugins/session_view/public/components/process_tree_alerts/index.tsx b/x-pack/plugins/session_view/public/components/process_tree_alerts/index.tsx index c97ccfe253605..b51d58bf825ec 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_alerts/index.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_alerts/index.tsx @@ -16,8 +16,7 @@ export interface ProcessTreeAlertsDeps { jumpToAlertID?: string; isProcessSelected?: boolean; onAlertSelected: (e: MouseEvent) => void; - loadAlertDetails?: (alertUuid: string, handleOnAlertDetailsClosed: () => void) => void; - handleOnAlertDetailsClosed: (alertUuid: string) => void; + onShowAlertDetails: (alertUuid: string) => void; } export function ProcessTreeAlerts({ @@ -25,8 +24,7 @@ export function ProcessTreeAlerts({ jumpToAlertID, isProcessSelected = false, onAlertSelected, - loadAlertDetails, - handleOnAlertDetailsClosed, + onShowAlertDetails, }: ProcessTreeAlertsDeps) { const [selectedAlert, setSelectedAlert] = useState(null); const styles = useStyles(); @@ -90,8 +88,7 @@ export function ProcessTreeAlerts({ isSelected={isProcessSelected && selectedAlert?.uuid === alertUuid} onClick={handleAlertClick} selectAlert={selectAlert} - loadAlertDetails={loadAlertDetails} - handleOnAlertDetailsClosed={handleOnAlertDetailsClosed} + onShowAlertDetails={onShowAlertDetails} /> ); })} diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx index 2e82e822f0c82..5c3b790ad0430 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_node/index.test.tsx @@ -36,7 +36,7 @@ describe('ProcessTreeNode component', () => { }, } as unknown as RefObject, onChangeJumpToEventVisibility: jest.fn(), - handleOnAlertDetailsClosed: (_alertUuid: string) => {}, + onShowAlertDetails: jest.fn(), }; beforeEach(() => { diff --git a/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx index b1c42dd95efb9..387e7a5074699 100644 --- a/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx +++ b/x-pack/plugins/session_view/public/components/process_tree_node/index.tsx @@ -21,7 +21,8 @@ import React, { useMemo, RefObject, } from 'react'; -import { EuiButton, EuiIcon, formatDate } from '@elastic/eui'; +import { EuiButton, EuiIcon, EuiToolTip, formatDate } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { Process } from '../../../common/types/process_tree'; import { useVisible } from '../../hooks/use_visible'; @@ -43,8 +44,7 @@ export interface ProcessDeps { verboseModeOn?: boolean; scrollerRef: RefObject; onChangeJumpToEventVisibility: (isVisible: boolean, isAbove: boolean) => void; - loadAlertDetails?: (alertUuid: string, handleOnAlertDetailsClosed: () => void) => void; - handleOnAlertDetailsClosed: (alertUuid: string) => void; + onShowAlertDetails: (alertUuid: string) => void; } /** @@ -62,8 +62,7 @@ export function ProcessTreeNode({ verboseModeOn = true, scrollerRef, onChangeJumpToEventVisibility, - loadAlertDetails, - handleOnAlertDetailsClosed, + onShowAlertDetails, }: ProcessDeps) { const textRef = useRef(null); @@ -144,6 +143,33 @@ export function ProcessTreeNode({ ); const processDetails = process.getDetails(); + const hasExec = process.hasExec(); + + const processIcon = useMemo(() => { + if (!process.parent) { + return 'unlink'; + } else if (hasExec) { + return 'console'; + } else { + return 'branch'; + } + }, [hasExec, process.parent]); + + const iconTooltip = useMemo(() => { + if (!process.parent) { + return i18n.translate('xpack.sessionView.processNode.tooltipOrphan', { + defaultMessage: 'Process missing parent (orphan)', + }); + } else if (hasExec) { + return i18n.translate('xpack.sessionView.processNode.tooltipExec', { + defaultMessage: "Process exec'd", + }); + } else { + return i18n.translate('xpack.sessionView.processNode.tooltipFork', { + defaultMessage: 'Process forked (no exec)', + }); + } + }, [hasExec, process.parent]); if (!processDetails?.process) { return null; @@ -169,11 +195,9 @@ export function ProcessTreeNode({ const showUserEscalation = user.id !== parent.user.id; const interactiveSession = !!tty; const sessionIcon = interactiveSession ? 'consoleApp' : 'compute'; - const hasExec = process.hasExec(); const iconTestSubj = hasExec ? 'sessionView:processTreeNodeExecIcon' : 'sessionView:processTreeNodeForkIcon'; - const processIcon = hasExec ? 'console' : 'branch'; const timeStampsNormal = formatDate(start, KIBANA_DATE_FORMAT); @@ -200,7 +224,9 @@ export function ProcessTreeNode({ ) : ( - + + + {' '} {workingDirectory}  {args[0]}  @@ -255,8 +281,7 @@ export function ProcessTreeNode({ jumpToAlertID={jumpToAlertID} isProcessSelected={selectedProcessId === process.id} onAlertSelected={onProcessClicked} - loadAlertDetails={loadAlertDetails} - handleOnAlertDetailsClosed={handleOnAlertDetailsClosed} + onShowAlertDetails={onShowAlertDetails} /> )} @@ -276,8 +301,7 @@ export function ProcessTreeNode({ verboseModeOn={verboseModeOn} scrollerRef={scrollerRef} onChangeJumpToEventVisibility={onChangeJumpToEventVisibility} - loadAlertDetails={loadAlertDetails} - handleOnAlertDetailsClosed={handleOnAlertDetailsClosed} + onShowAlertDetails={onShowAlertDetails} /> ); })} diff --git a/x-pack/plugins/session_view/public/components/session_view/hooks.ts b/x-pack/plugins/session_view/public/components/session_view/hooks.ts index a134a366c4168..bf8796336602d 100644 --- a/x-pack/plugins/session_view/public/components/session_view/hooks.ts +++ b/x-pack/plugins/session_view/public/components/session_view/hooks.ts @@ -15,9 +15,11 @@ import { ProcessEventResults, } from '../../../common/types/process_tree'; import { + ALERTS_ROUTE, PROCESS_EVENTS_ROUTE, PROCESS_EVENTS_PER_PAGE, ALERT_STATUS_ROUTE, + QUERY_KEY_PROCESS_EVENTS, QUERY_KEY_ALERTS, } from '../../../common/constants'; @@ -28,9 +30,10 @@ export const useFetchSessionViewProcessEvents = ( const { http } = useKibana().services; const [isJumpToFirstPage, setIsJumpToFirstPage] = useState(false); const jumpToCursor = jumpToEvent && jumpToEvent.process.start; + const cachingKeys = [QUERY_KEY_PROCESS_EVENTS, sessionEntityId]; const query = useInfiniteQuery( - 'sessionViewProcessEvents', + cachingKeys, async ({ pageParam = {} }) => { let { cursor } = pageParam; const { forward } = pageParam; @@ -52,7 +55,7 @@ export const useFetchSessionViewProcessEvents = ( return { events, cursor }; }, { - getNextPageParam: (lastPage, pages) => { + getNextPageParam: (lastPage) => { if (lastPage.events.length === PROCESS_EVENTS_PER_PAGE) { return { cursor: lastPage.events[lastPage.events.length - 1]['@timestamp'], @@ -60,7 +63,7 @@ export const useFetchSessionViewProcessEvents = ( }; } }, - getPreviousPageParam: (firstPage, pages) => { + getPreviousPageParam: (firstPage) => { if (jumpToEvent && firstPage.events.length === PROCESS_EVENTS_PER_PAGE) { return { cursor: firstPage.events[0]['@timestamp'], @@ -84,6 +87,32 @@ export const useFetchSessionViewProcessEvents = ( return query; }; +export const useFetchSessionViewAlerts = (sessionEntityId: string) => { + const { http } = useKibana().services; + const cachingKeys = [QUERY_KEY_ALERTS, sessionEntityId]; + const query = useQuery( + cachingKeys, + async () => { + const res = await http.get(ALERTS_ROUTE, { + query: { + sessionEntityId, + }, + }); + + const events = res.events.map((event: any) => event._source as ProcessEvent); + + return events; + }, + { + refetchOnWindowFocus: false, + refetchOnMount: false, + refetchOnReconnect: false, + } + ); + + return query; +}; + export const useFetchAlertStatus = ( updatedAlertsStatus: AlertStatusEventEntityIdMap, alertUuid: string diff --git a/x-pack/plugins/session_view/public/components/session_view/index.tsx b/x-pack/plugins/session_view/public/components/session_view/index.tsx index af4eb6114a0a2..ee481c4204108 100644 --- a/x-pack/plugins/session_view/public/components/session_view/index.tsx +++ b/x-pack/plugins/session_view/public/components/session_view/index.tsx @@ -23,7 +23,11 @@ import { SessionViewDetailPanel } from '../session_view_detail_panel'; import { SessionViewSearchBar } from '../session_view_search_bar'; import { SessionViewDisplayOptions } from '../session_view_display_options'; import { useStyles } from './styles'; -import { useFetchAlertStatus, useFetchSessionViewProcessEvents } from './hooks'; +import { + useFetchAlertStatus, + useFetchSessionViewProcessEvents, + useFetchSessionViewAlerts, +} from './hooks'; /** * The main wrapper component for the session view. @@ -61,8 +65,12 @@ export const SessionView = ({ hasPreviousPage, } = useFetchSessionViewProcessEvents(sessionEntityId, jumpToEvent); - const hasData = data && data.pages.length > 0 && data.pages[0].events.length > 0; - const renderIsLoading = isFetching && !data; + const alertsQuery = useFetchSessionViewAlerts(sessionEntityId); + const { data: alerts, error: alertsError, isFetching: alertsFetching } = alertsQuery; + + const hasData = alerts && data && data.pages?.[0].events.length > 0; + const hasError = error || alertsError; + const renderIsLoading = (isFetching || alertsFetching) && !data; const renderDetails = isDetailOpen && selectedProcess; const { data: newUpdatedAlertsStatus } = useFetchAlertStatus( updatedAlertsStatus, @@ -83,6 +91,15 @@ export const SessionView = ({ setIsDetailOpen(!isDetailOpen); }, [isDetailOpen]); + const onShowAlertDetails = useCallback( + (alertUuid: string) => { + if (loadAlertDetails) { + loadAlertDetails(alertUuid, () => handleOnAlertDetailsClosed(alertUuid)); + } + }, + [loadAlertDetails, handleOnAlertDetailsClosed] + ); + const handleOptionChange = useCallback((checkedOptions: DisplayOptionsState) => { setDisplayOptions(checkedOptions); }, []); @@ -165,7 +182,7 @@ export const SessionView = ({ )} - {error && ( + {hasError && ( @@ -215,7 +232,7 @@ export const SessionView = ({ {renderDetails ? ( <> - + diff --git a/x-pack/plugins/session_view/public/components/session_view/styles.ts b/x-pack/plugins/session_view/public/components/session_view/styles.ts index d2c87130bfa4b..edfe2356d5aa2 100644 --- a/x-pack/plugins/session_view/public/components/session_view/styles.ts +++ b/x-pack/plugins/session_view/public/components/session_view/styles.ts @@ -17,6 +17,10 @@ export const useStyles = ({ height = 500 }: StylesDeps) => { const { euiTheme } = useEuiTheme(); const cached = useMemo(() => { + const { border, colors } = euiTheme; + + const thinBorder = `${border.width.thin} solid ${colors.lightShade}!important`; + const processTree: CSSObject = { height: `${height}px`, position: 'relative', @@ -24,6 +28,12 @@ export const useStyles = ({ height = 500 }: StylesDeps) => { const detailPanel: CSSObject = { height: `${height}px`, + borderLeft: thinBorder, + borderRight: thinBorder, + }; + + const resizeHandle: CSSObject = { + zIndex: 2, }; const searchBar: CSSObject = { @@ -38,6 +48,7 @@ export const useStyles = ({ height = 500 }: StylesDeps) => { return { processTree, detailPanel, + resizeHandle, searchBar, buttonsEyeDetail, }; diff --git a/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.test.tsx b/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.test.tsx index f754086fe5fab..40e71efd8a6cf 100644 --- a/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.test.tsx +++ b/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.test.tsx @@ -6,7 +6,10 @@ */ import React from 'react'; -import { sessionViewBasicProcessMock } from '../../../common/mocks/constants/session_view_process.mock'; +import { + mockAlerts, + sessionViewBasicProcessMock, +} from '../../../common/mocks/constants/session_view_process.mock'; import { AppContextTestRender, createAppRootMockRenderer } from '../../test'; import { SessionViewDetailPanel } from './index'; @@ -14,27 +17,66 @@ describe('SessionView component', () => { let render: () => ReturnType; let renderResult: ReturnType; let mockedContext: AppContextTestRender; + let mockOnProcessSelected = jest.fn((process) => process); + let mockShowAlertDetails = jest.fn((alertId) => alertId); beforeEach(() => { mockedContext = createAppRootMockRenderer(); + mockOnProcessSelected = jest.fn((process) => process); + mockShowAlertDetails = jest.fn((alertId) => alertId); }); describe('When SessionViewDetailPanel is mounted', () => { it('shows process detail by default', async () => { renderResult = mockedContext.render( - + ); expect(renderResult.queryByText('8e4daeb2-4a4e-56c4-980e-f0dcfdbc3726')).toBeVisible(); }); it('can switch tabs to show host details', async () => { renderResult = mockedContext.render( - + ); renderResult.queryByText('Host')?.click(); expect(renderResult.queryByText('hostname')).toBeVisible(); expect(renderResult.queryAllByText('james-fleet-714-2')).toHaveLength(2); }); + + it('can switch tabs to show alert details', async () => { + renderResult = mockedContext.render( + + ); + + renderResult.queryByText('Alerts')?.click(); + expect(renderResult.queryByText('List view')).toBeVisible(); + }); + it('alert tab disabled when no alerts', async () => { + renderResult = mockedContext.render( + + ); + + renderResult.queryByText('Alerts')?.click(); + expect(renderResult.queryByText('List view')).toBeFalsy(); + }); }); }); diff --git a/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx b/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx index a47ce1d91ac97..51eb65a38f835 100644 --- a/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx +++ b/x-pack/plugins/session_view/public/components/session_view_detail_panel/index.tsx @@ -6,50 +6,91 @@ */ import React, { useState, useMemo, useCallback } from 'react'; import { EuiTabs, EuiTab, EuiNotificationBadge } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { EuiTabProps } from '../../types'; -import { Process } from '../../../common/types/process_tree'; +import { Process, ProcessEvent } from '../../../common/types/process_tree'; import { getDetailPanelProcess, getSelectedTabContent } from './helpers'; import { DetailPanelProcessTab } from '../detail_panel_process_tab'; import { DetailPanelHostTab } from '../detail_panel_host_tab'; +import { DetailPanelAlertTab } from '../detail_panel_alert_tab'; +import { ALERT_COUNT_THRESHOLD } from '../../../common/constants'; interface SessionViewDetailPanelDeps { selectedProcess: Process; - onProcessSelected?: (process: Process) => void; + alerts?: ProcessEvent[]; + investigatedAlert?: ProcessEvent; + onProcessSelected: (process: Process) => void; + onShowAlertDetails: (alertId: string) => void; } /** * Detail panel in the session view. */ -export const SessionViewDetailPanel = ({ selectedProcess }: SessionViewDetailPanelDeps) => { +export const SessionViewDetailPanel = ({ + alerts, + selectedProcess, + investigatedAlert, + onProcessSelected, + onShowAlertDetails, +}: SessionViewDetailPanelDeps) => { const [selectedTabId, setSelectedTabId] = useState('process'); const processDetail = useMemo(() => getDetailPanelProcess(selectedProcess), [selectedProcess]); - const tabs: EuiTabProps[] = useMemo( - () => [ + const alertsCount = useMemo(() => { + if (!alerts) { + return 0; + } + + return alerts.length >= ALERT_COUNT_THRESHOLD ? ALERT_COUNT_THRESHOLD + '+' : alerts.length; + }, [alerts]); + + const tabs: EuiTabProps[] = useMemo(() => { + const hasAlerts = !!alerts?.length; + + return [ { id: 'process', - name: 'Process', + name: i18n.translate('xpack.sessionView.detailsPanel.process', { + defaultMessage: 'Process', + }), content: , }, { id: 'host', - name: 'Host', + name: i18n.translate('xpack.sessionView.detailsPanel.host', { + defaultMessage: 'Host', + }), content: , }, { id: 'alerts', - disabled: true, - name: 'Alerts', - append: ( + name: i18n.translate('xpack.sessionView.detailsPanel.alerts', { + defaultMessage: 'Alerts', + }), + append: hasAlerts && ( - 10 + {alertsCount} ), - content: null, + content: alerts && ( + + ), }, - ], - [processDetail, selectedProcess.events] - ); + ]; + }, [ + alerts, + alertsCount, + processDetail, + selectedProcess.events, + onProcessSelected, + onShowAlertDetails, + investigatedAlert, + ]); const onSelectedTabChanged = useCallback((id: string) => { setSelectedTabId(id); diff --git a/x-pack/plugins/session_view/server/plugin.ts b/x-pack/plugins/session_view/server/plugin.ts index c7fd511b3de05..7347f7676af62 100644 --- a/x-pack/plugins/session_view/server/plugin.ts +++ b/x-pack/plugins/session_view/server/plugin.ts @@ -11,12 +11,14 @@ import { Plugin, Logger, PluginInitializerContext, + IRouter, } from '../../../../src/core/server'; import { SessionViewSetupPlugins, SessionViewStartPlugins } from './types'; import { registerRoutes } from './routes'; export class SessionViewPlugin implements Plugin { private logger: Logger; + private router: IRouter | undefined; /** * Initialize SessionViewPlugin class properties (logger, etc) that is accessible @@ -28,14 +30,16 @@ export class SessionViewPlugin implements Plugin { public setup(core: CoreSetup, plugins: SessionViewSetupPlugins) { this.logger.debug('session view: Setup'); - const router = core.http.createRouter(); - - // Register server routes - registerRoutes(router); + this.router = core.http.createRouter(); } public start(core: CoreStart, plugins: SessionViewStartPlugins) { this.logger.debug('session view: Start'); + + // Register server routes + if (this.router) { + registerRoutes(this.router, plugins.ruleRegistry); + } } public stop() { diff --git a/x-pack/plugins/session_view/server/routes/alerts_route.test.ts b/x-pack/plugins/session_view/server/routes/alerts_route.test.ts new file mode 100644 index 0000000000000..4c8ee6fb2c83e --- /dev/null +++ b/x-pack/plugins/session_view/server/routes/alerts_route.test.ts @@ -0,0 +1,133 @@ +/* + * 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 { + ALERT_RULE_CONSUMER, + ALERT_RULE_TYPE_ID, + SPACE_IDS, + ALERT_WORKFLOW_STATUS, +} from '@kbn/rule-data-utils'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; +import { doSearch } from './alerts_route'; +import { mockEvents } from '../../common/mocks/constants/session_view_process.mock'; + +import { + AlertsClient, + ConstructorOptions, +} from '../../../rule_registry/server/alert_data_client/alerts_client'; +import { loggingSystemMock } from 'src/core/server/mocks'; +import { alertingAuthorizationMock } from '../../../alerting/server/authorization/alerting_authorization.mock'; +import { auditLoggerMock } from '../../../security/server/audit/mocks'; +import { AlertingAuthorizationEntity } from '../../../alerting/server'; +import { ruleDataServiceMock } from '../../../rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.mock'; + +const alertingAuthMock = alertingAuthorizationMock.create(); +const auditLogger = auditLoggerMock.create(); + +const DEFAULT_SPACE = 'test_default_space_id'; + +const getEmptyResponse = async () => { + return { + hits: { + total: 0, + hits: [], + }, + }; +}; + +const getResponse = async () => { + return { + hits: { + total: mockEvents.length, + hits: mockEvents.map((event) => { + return { + found: true, + _type: 'alert', + _index: '.alerts-security', + _id: 'NoxgpHkBqbdrfX07MqXV', + _version: 1, + _seq_no: 362, + _primary_term: 2, + _source: { + [ALERT_RULE_TYPE_ID]: 'apm.error_rate', + message: 'hello world 1', + [ALERT_RULE_CONSUMER]: 'apm', + [ALERT_WORKFLOW_STATUS]: 'open', + [SPACE_IDS]: ['test_default_space_id'], + ...event, + }, + }; + }), + }, + }; +}; + +const esClientMock = elasticsearchServiceMock.createElasticsearchClient(getResponse()); + +const alertsClientParams: jest.Mocked = { + logger: loggingSystemMock.create().get(), + authorization: alertingAuthMock, + auditLogger, + ruleDataService: ruleDataServiceMock.create(), + esClient: esClientMock, +}; + +describe('alerts_route.ts', () => { + beforeEach(() => { + jest.resetAllMocks(); + + alertingAuthMock.getSpaceId.mockImplementation(() => DEFAULT_SPACE); + // @ts-expect-error + alertingAuthMock.getAuthorizationFilter.mockImplementation(async () => + Promise.resolve({ filter: [] }) + ); + // @ts-expect-error + alertingAuthMock.getAugmentedRuleTypesWithAuthorization.mockImplementation(async () => { + const authorizedRuleTypes = new Set(); + authorizedRuleTypes.add({ producer: 'apm' }); + return Promise.resolve({ authorizedRuleTypes }); + }); + + alertingAuthMock.ensureAuthorized.mockImplementation( + // @ts-expect-error + async ({ + ruleTypeId, + consumer, + operation, + entity, + }: { + ruleTypeId: string; + consumer: string; + operation: string; + entity: typeof AlertingAuthorizationEntity.Alert; + }) => { + if (ruleTypeId === 'apm.error_rate' && consumer === 'apm') { + return Promise.resolve(); + } + return Promise.reject(new Error(`Unauthorized for ${ruleTypeId} and ${consumer}`)); + } + ); + }); + + describe('doSearch(client, sessionEntityId)', () => { + it('should return an empty events array for a non existant entity_id', async () => { + const esClient = elasticsearchServiceMock.createElasticsearchClient(getEmptyResponse()); + const alertsClient = new AlertsClient({ ...alertsClientParams, esClient }); + const body = await doSearch(alertsClient, 'asdf'); + + expect(body.events.length).toBe(0); + }); + + it('returns results for a particular session entity_id', async () => { + const esClient = elasticsearchServiceMock.createElasticsearchClient(getResponse()); + const alertsClient = new AlertsClient({ ...alertsClientParams, esClient }); + + const body = await doSearch(alertsClient, 'asdf'); + + expect(body.events.length).toBe(mockEvents.length); + }); + }); +}); diff --git a/x-pack/plugins/session_view/server/routes/alerts_route.ts b/x-pack/plugins/session_view/server/routes/alerts_route.ts new file mode 100644 index 0000000000000..3d03cb5cb8214 --- /dev/null +++ b/x-pack/plugins/session_view/server/routes/alerts_route.ts @@ -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 { schema } from '@kbn/config-schema'; +import { IRouter } from '../../../../../src/core/server'; +import { + ALERTS_ROUTE, + ALERTS_PER_PAGE, + ENTRY_SESSION_ENTITY_ID_PROPERTY, +} from '../../common/constants'; +import { expandDottedObject } from '../../common/utils/expand_dotted_object'; +import type { AlertsClient, RuleRegistryPluginStartContract } from '../../../rule_registry/server'; + +export const registerAlertsRoute = ( + router: IRouter, + ruleRegistry: RuleRegistryPluginStartContract +) => { + router.get( + { + path: ALERTS_ROUTE, + validate: { + query: schema.object({ + sessionEntityId: schema.string(), + }), + }, + }, + async (context, request, response) => { + const client = await ruleRegistry.getRacClientWithRequest(request); + const { sessionEntityId } = request.query; + const body = await doSearch(client, sessionEntityId); + + return response.ok({ body }); + } + ); +}; + +export const doSearch = async (client: AlertsClient, sessionEntityId: string) => { + const indices = await client.getAuthorizedAlertsIndices(['siem']); + + if (!indices) { + return { events: [] }; + } + + const results = await client.find({ + query: { + match: { + [ENTRY_SESSION_ENTITY_ID_PROPERTY]: sessionEntityId, + }, + }, + track_total_hits: false, + size: ALERTS_PER_PAGE, + index: indices.join(','), + }); + + const events = results.hits.hits.map((hit: any) => { + // the alert indexes flattens many properties. this util unflattens them as session view expects structured json. + hit._source = expandDottedObject(hit._source); + + return hit; + }); + + return { events }; +}; diff --git a/x-pack/plugins/session_view/server/routes/index.ts b/x-pack/plugins/session_view/server/routes/index.ts index b8cb80dc1d1d4..17efeb5d07a7b 100644 --- a/x-pack/plugins/session_view/server/routes/index.ts +++ b/x-pack/plugins/session_view/server/routes/index.ts @@ -6,11 +6,14 @@ */ import { IRouter } from '../../../../../src/core/server'; import { registerProcessEventsRoute } from './process_events_route'; +import { registerAlertsRoute } from './alerts_route'; import { registerAlertStatusRoute } from './alert_status_route'; import { sessionEntryLeadersRoute } from './session_entry_leaders_route'; +import { RuleRegistryPluginStartContract } from '../../../rule_registry/server'; -export const registerRoutes = (router: IRouter) => { +export const registerRoutes = (router: IRouter, ruleRegistry: RuleRegistryPluginStartContract) => { registerProcessEventsRoute(router); registerAlertStatusRoute(router); sessionEntryLeadersRoute(router); + registerAlertsRoute(router, ruleRegistry); }; diff --git a/x-pack/plugins/session_view/server/routes/process_events_route.ts b/x-pack/plugins/session_view/server/routes/process_events_route.ts index 47e2d917733d5..7be1885c70ab1 100644 --- a/x-pack/plugins/session_view/server/routes/process_events_route.ts +++ b/x-pack/plugins/session_view/server/routes/process_events_route.ts @@ -11,10 +11,8 @@ import { PROCESS_EVENTS_ROUTE, PROCESS_EVENTS_PER_PAGE, PROCESS_EVENTS_INDEX, - ALERTS_INDEX, ENTRY_SESSION_ENTITY_ID_PROPERTY, } from '../../common/constants'; -import { expandDottedObject } from '../../common/utils/expand_dotted_object'; export const registerProcessEventsRoute = (router: IRouter) => { router.get( @@ -45,35 +43,25 @@ export const doSearch = async ( forward = true ) => { const search = await client.search({ - // TODO: move alerts into it's own route with it's own pagination. - index: [PROCESS_EVENTS_INDEX, ALERTS_INDEX], - ignore_unavailable: true, + index: [PROCESS_EVENTS_INDEX], body: { query: { match: { [ENTRY_SESSION_ENTITY_ID_PROPERTY]: sessionEntityId, }, }, - // This runtime_mappings is a temporary fix, so we are able to Query these ECS fields while they are not available - // TODO: Remove the runtime_mappings once process.entry_leader.entity_id is implemented to ECS - runtime_mappings: { - [ENTRY_SESSION_ENTITY_ID_PROPERTY]: { - type: 'keyword', - }, - }, size: PROCESS_EVENTS_PER_PAGE, - sort: [{ '@timestamp': forward ? 'asc' : 'desc' }], + // we first sort by process.start, this allows lifecycle events to load all at once for a given process, and + // avoid issues like where the session leaders 'end' event is loaded at the very end of what could be multiple pages of events + sort: [ + { 'process.start': forward ? 'asc' : 'desc' }, + { '@timestamp': forward ? 'asc' : 'desc' }, + ], search_after: cursor ? [cursor] : undefined, }, }); - const events = search.hits.hits.map((hit: any) => { - // TODO: re-eval if this is needed after moving alerts to it's own route. - // the .siem-signals-default index flattens many properties. this util unflattens them. - hit._source = expandDottedObject(hit._source); - - return hit; - }); + const events = search.hits.hits; if (!forward) { events.reverse(); diff --git a/x-pack/plugins/session_view/server/types.ts b/x-pack/plugins/session_view/server/types.ts index 0d1375081ca87..29995077ccfbe 100644 --- a/x-pack/plugins/session_view/server/types.ts +++ b/x-pack/plugins/session_view/server/types.ts @@ -4,8 +4,15 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { + RuleRegistryPluginSetupContract as RuleRegistryPluginSetup, + RuleRegistryPluginStartContract as RuleRegistryPluginStart, +} from '../../rule_registry/server'; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface SessionViewSetupPlugins {} -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface SessionViewStartPlugins {} +export interface SessionViewSetupPlugins { + ruleRegistry: RuleRegistryPluginSetup; +} + +export interface SessionViewStartPlugins { + ruleRegistry: RuleRegistryPluginStart; +} diff --git a/x-pack/plugins/session_view/tsconfig.json b/x-pack/plugins/session_view/tsconfig.json index a99e83976a31d..0a21d320dfb29 100644 --- a/x-pack/plugins/session_view/tsconfig.json +++ b/x-pack/plugins/session_view/tsconfig.json @@ -37,6 +37,7 @@ { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, { "path": "../../../src/plugins/es_ui_shared/tsconfig.json" }, { "path": "../infra/tsconfig.json" }, - { "path": "../../../src/plugins/kibana_utils/tsconfig.json" } + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../rule_registry/tsconfig.json" } ] } From d253355234e2b1b393ec7e6dc10641edfe8f900c Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Wed, 23 Mar 2022 11:46:42 -0400 Subject: [PATCH 02/13] [SearchProfiler] Handle scenario when user has no indices (#128066) --- .../application/hooks/use_request_profile.ts | 31 ++++++++- .../apps/dev_tools/searchprofiler_editor.ts | 64 +++++++++++++++---- x-pack/test/functional/config.js | 8 +++ 3 files changed, 89 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/searchprofiler/public/application/hooks/use_request_profile.ts b/x-pack/plugins/searchprofiler/public/application/hooks/use_request_profile.ts index c27ca90e6e2f2..7f5d31b781310 100644 --- a/x-pack/plugins/searchprofiler/public/application/hooks/use_request_profile.ts +++ b/x-pack/plugins/searchprofiler/public/application/hooks/use_request_profile.ts @@ -21,6 +21,16 @@ interface ReturnValue { error?: string; } +interface ProfileResponse { + profile?: { shards: ShardSerialized[] }; + _shards: { + failed: number; + skipped: number; + total: number; + successful: number; + }; +} + const extractProfilerErrorMessage = (e: any): string | undefined => { if (e.body?.attributes?.error?.reason) { const { reason, line, col } = e.body.attributes.error; @@ -67,8 +77,7 @@ export const useRequestProfile = () => { try { const resp = await http.post< - | { ok: true; resp: { profile: { shards: ShardSerialized[] } } } - | { ok: false; err: { msg: string } } + { ok: true; resp: ProfileResponse } | { ok: false; err: { msg: string } } >('../api/searchprofiler/profile', { body: JSON.stringify(payload), headers: { 'Content-Type': 'application/json' }, @@ -78,7 +87,23 @@ export const useRequestProfile = () => { return { data: null, error: resp.err.msg }; } - return { data: resp.resp.profile.shards }; + // If a user attempts to run Search Profiler without any indices, + // _shards=0 and a "profile" output will not be returned + if (resp.resp._shards.total === 0) { + notifications.addDanger({ + 'data-test-subj': 'noShardsNotification', + title: i18n.translate('xpack.searchProfiler.errorNoShardsTitle', { + defaultMessage: 'Unable to profile', + }), + text: i18n.translate('xpack.searchProfiler.errorNoShardsDescription', { + defaultMessage: 'Verify your index input matches a valid index', + }), + }); + + return { data: null }; + } + + return { data: resp.resp.profile!.shards }; } catch (e) { const profilerErrorMessage = extractProfilerErrorMessage(e); if (profilerErrorMessage) { diff --git a/x-pack/test/functional/apps/dev_tools/searchprofiler_editor.ts b/x-pack/test/functional/apps/dev_tools/searchprofiler_editor.ts index 3ab27e52477a6..9a2968a1fd8b5 100644 --- a/x-pack/test/functional/apps/dev_tools/searchprofiler_editor.ts +++ b/x-pack/test/functional/apps/dev_tools/searchprofiler_editor.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import { asyncForEach } from '@kbn/std'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { @@ -14,6 +15,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const aceEditor = getService('aceEditor'); const retry = getService('retry'); const security = getService('security'); + const es = getService('es'); + const log = getService('log'); const editorTestSubjectSelector = 'searchProfilerEditor'; @@ -34,23 +37,23 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const okInput = [ `{ -"query": { -"match_all": {}`, + "query": { + "match_all": {}`, `{ -"query": { -"match_all": { -"test": """{ "more": "json" }"""`, + "query": { + "match_all": { + "test": """{ "more": "json" }"""`, ]; const notOkInput = [ `{ -"query": { -"match_all": { -"test": """{ "more": "json" }""`, + "query": { + "match_all": { + "test": """{ "more": "json" }""`, `{ -"query": { -"match_all": { -"test": """{ "more": "json" }""'`, + "query": { + "match_all": { + "test": """{ "more": "json" }""'`, ]; const expectHasParseErrorsToBe = (expectation: boolean) => async (inputs: string[]) => { @@ -70,5 +73,44 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await expectHasParseErrorsToBe(false)(okInput); await expectHasParseErrorsToBe(true)(notOkInput); }); + + describe('No indices', () => { + before(async () => { + // Delete any existing indices that were not properly cleaned up + try { + const indices = await es.indices.get({ + index: '*', + }); + const indexNames = Object.keys(indices); + + if (indexNames.length > 0) { + await asyncForEach(indexNames, async (indexName) => { + await es.indices.delete({ index: indexName }); + }); + } + } catch (e) { + log.debug('[Setup error] Error deleting existing indices'); + throw e; + } + }); + + it('returns error if profile is executed with no valid indices', async () => { + const input = { + query: { + match_all: {}, + }, + }; + + await aceEditor.setValue(editorTestSubjectSelector, JSON.stringify(input)); + + await testSubjects.click('profileButton'); + + await retry.waitFor('notification renders', async () => { + const notification = await testSubjects.find('noShardsNotification'); + const notificationText = await notification.getVisibleText(); + return notificationText.includes('Unable to profile'); + }); + }); + }); }); } diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 28000c3d4bac8..b7774b463d058 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -425,6 +425,14 @@ export default async function ({ readConfigFile }) { }, global_devtools_read: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['read', 'all'], + }, + ], + }, kibana: [ { feature: { From 09f78b01b966854d63b5cd7c79e36c9f35bbd580 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 23 Mar 2022 09:33:28 -0700 Subject: [PATCH 03/13] skip suite failing es promotion (#128396) --- x-pack/test/functional/apps/lens/show_underlying_data.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/lens/show_underlying_data.ts b/x-pack/test/functional/apps/lens/show_underlying_data.ts index d6ae299baceaf..2444e8714e014 100644 --- a/x-pack/test/functional/apps/lens/show_underlying_data.ts +++ b/x-pack/test/functional/apps/lens/show_underlying_data.ts @@ -16,7 +16,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const find = getService('find'); const browser = getService('browser'); - describe('show underlying data', () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/128396 + describe.skip('show underlying data', () => { it('should show the open button for a compatible saved visualization', async () => { await PageObjects.visualize.gotoVisualizationLandingPage(); await listingTable.searchForItemWithName('lnsXYvis'); From 2f06801f8ee18b2cdd7ce2280530fe8be479eb6c Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Wed, 23 Mar 2022 12:58:41 -0400 Subject: [PATCH 04/13] [Fleet] Fix refresh assets tab on package install (#128285) --- .../integrations/sections/epm/screens/detail/index.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx index d002a743e77bc..dbd1c71da3d1b 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx @@ -144,9 +144,13 @@ export function Detail() { // Refresh package info when status change const [oldPackageInstallStatus, setOldPackageStatus] = useState(packageInstallStatus); + useEffect(() => { + if (packageInstallStatus === 'not_installed') { + setOldPackageStatus(packageInstallStatus); + } if (oldPackageInstallStatus === 'not_installed' && packageInstallStatus === 'installed') { - setOldPackageStatus(oldPackageInstallStatus); + setOldPackageStatus(packageInstallStatus); refreshPackageInfo(); } }, [packageInstallStatus, oldPackageInstallStatus, refreshPackageInfo]); From 42e6cee204043b97eda251b5fefffdaf4008ce43 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Wed, 23 Mar 2022 19:00:00 +0100 Subject: [PATCH 05/13] [Cases] Select case modal hook hides closed and all dropdown filters by default (#128380) --- .../use_cases_add_to_existing_case_modal.test.tsx | 4 ++++ .../selector_modal/use_cases_add_to_existing_case_modal.tsx | 2 ++ 2 files changed, 6 insertions(+) diff --git a/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.test.tsx b/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.test.tsx index df40ccd3b1e90..b0e316e891744 100644 --- a/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.test.tsx @@ -9,6 +9,7 @@ import { renderHook } from '@testing-library/react-hooks'; import React from 'react'; +import { CaseStatuses, StatusAll } from '../../../../common'; import { CasesContext } from '../../cases_context'; import { CasesContextStoreActionsList } from '../../cases_context/cases_context_reducer'; import { useCasesAddToExistingCaseModal } from './use_cases_add_to_existing_case_modal'; @@ -62,6 +63,9 @@ describe('use cases add to existing case modal hook', () => { expect(dispatch).toHaveBeenCalledWith( expect.objectContaining({ type: CasesContextStoreActionsList.OPEN_ADD_TO_CASE_MODAL, + payload: expect.objectContaining({ + hiddenStatuses: [CaseStatuses.closed, StatusAll], + }), }) ); }); diff --git a/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.tsx b/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.tsx index 5341f5be4183d..1e65fee4565b2 100644 --- a/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/selector_modal/use_cases_add_to_existing_case_modal.tsx @@ -6,6 +6,7 @@ */ import { useCallback } from 'react'; +import { CaseStatuses, StatusAll } from '../../../../common'; import { AllCasesSelectorModalProps } from '.'; import { useCasesToast } from '../../../common/use_cases_toast'; import { Case } from '../../../containers/types'; @@ -44,6 +45,7 @@ export const useCasesAddToExistingCaseModal = (props: AllCasesSelectorModalProps type: CasesContextStoreActionsList.OPEN_ADD_TO_CASE_MODAL, payload: { ...props, + hiddenStatuses: [CaseStatuses.closed, StatusAll], onRowClick: (theCase?: Case) => { // when the case is undefined in the modal // the user clicked "create new case" From f49f58614f3e6fe2310f61d19a6571b49f4053a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=BCrkan=20YALAMAN?= Date: Wed, 23 Mar 2022 19:34:03 +0100 Subject: [PATCH 06/13] [App Search] Fix sorting options for elasticsearch index based engines (#128384) * Fix sorting options for elasticsearch index based engines * review changes and missing translation changes --- .../build_search_ui_config.ts | 12 +++--- .../search_experience/search_experience.tsx | 40 +++++++++++++++---- .../app_search/components/engine/types.ts | 1 + .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 6 files changed, 41 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.ts index 25342f24cc872..9c06527162b81 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.ts @@ -9,7 +9,12 @@ import { Schema } from '../../../../shared/schema/types'; import { Fields } from './types'; -export const buildSearchUIConfig = (apiConnector: object, schema: Schema, fields: Fields) => { +export const buildSearchUIConfig = ( + apiConnector: object, + schema: Schema, + fields: Fields, + initialState = { sortDirection: 'desc', sortField: 'id' } +) => { const facets = fields.filterFields.reduce( (facetsConfig, fieldName) => ({ ...facetsConfig, @@ -22,10 +27,7 @@ export const buildSearchUIConfig = (apiConnector: object, schema: Schema, fields alwaysSearchOnInitialLoad: true, apiConnector, trackUrlState: false, - initialState: { - sortDirection: 'desc', - sortField: 'id', - }, + initialState, searchQuery: { disjunctiveFacets: fields.filterFields, facets, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx index ed2a1ed54f06d..52e0acbc81520 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx @@ -31,25 +31,39 @@ import { SearchExperienceContent } from './search_experience_content'; import { Fields, SortOption } from './types'; import { SearchBoxView, SortingView, MultiCheckboxFacetsView } from './views'; -const RECENTLY_UPLOADED = i18n.translate( - 'xpack.enterpriseSearch.appSearch.documents.search.sortBy.option.recentlyUploaded', +const DOCUMENT_ID = i18n.translate( + 'xpack.enterpriseSearch.appSearch.documents.search.sortBy.option.documentId', { - defaultMessage: 'Recently Uploaded', + defaultMessage: 'Document ID', } ); + +const RELEVANCE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.documents.search.sortBy.option.relevance', + { defaultMessage: 'Relevance' } +); + const DEFAULT_SORT_OPTIONS: SortOption[] = [ { - name: DESCENDING(RECENTLY_UPLOADED), + name: DESCENDING(DOCUMENT_ID), value: 'id', direction: 'desc', }, { - name: ASCENDING(RECENTLY_UPLOADED), + name: ASCENDING(DOCUMENT_ID), value: 'id', direction: 'asc', }, ]; +const RELEVANCE_SORT_OPTIONS: SortOption[] = [ + { + name: RELEVANCE, + value: '_score', + direction: 'desc', + }, +]; + export const SearchExperience: React.FC = () => { const { engine } = useValues(EngineLogic); const { http } = useValues(HttpLogic); @@ -66,8 +80,10 @@ export const SearchExperience: React.FC = () => { sortFields: [], } ); + const sortOptions = + engine.type === 'elasticsearch' ? RELEVANCE_SORT_OPTIONS : DEFAULT_SORT_OPTIONS; - const sortingOptions = buildSortOptions(fields, DEFAULT_SORT_OPTIONS); + const sortingOptions = buildSortOptions(fields, sortOptions); const connector = new AppSearchAPIConnector({ cacheResponses: false, @@ -78,7 +94,17 @@ export const SearchExperience: React.FC = () => { }, }); - const searchProviderConfig = buildSearchUIConfig(connector, engine.schema || {}, fields); + const initialState = { + sortField: engine.type === 'elasticsearch' ? '_score' : 'id', + sortDirection: 'desc', + }; + + const searchProviderConfig = buildSearchUIConfig( + connector, + engine.schema || {}, + fields, + initialState + ); return (
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts index 6faa749f95864..acdeed4854ecd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts @@ -12,6 +12,7 @@ export enum EngineTypes { default = 'default', indexed = 'indexed', meta = 'meta', + elasticsearch = 'elasticsearch', } export interface Engine { name: string; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 8914efcf12ded..db10095ce0591 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -8729,7 +8729,6 @@ "xpack.enterpriseSearch.appSearch.documents.search.sortBy.ariaLabel": "Trier les résultats par", "xpack.enterpriseSearch.appSearch.documents.search.sortBy.option.ascendingDropDownOptionLabel": "{fieldName} (croiss.)", "xpack.enterpriseSearch.appSearch.documents.search.sortBy.option.descendingDropDownOptionLabel": "{fieldName} (décroiss.)", - "xpack.enterpriseSearch.appSearch.documents.search.sortBy.option.recentlyUploaded": "Récemment chargé", "xpack.enterpriseSearch.appSearch.documents.title": "Documents", "xpack.enterpriseSearch.appSearch.editorRoleTypeDescription": "Les éditeurs peuvent gérer les paramètres de recherche.", "xpack.enterpriseSearch.appSearch.emptyState.createFirstEngineCta": "Créer un moteur", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 48f0d74d73765..f1ab772dbb243 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10279,7 +10279,6 @@ "xpack.enterpriseSearch.appSearch.documents.search.sortBy.ariaLabel": "結果の並べ替え条件", "xpack.enterpriseSearch.appSearch.documents.search.sortBy.option.ascendingDropDownOptionLabel": "{fieldName}(昇順)", "xpack.enterpriseSearch.appSearch.documents.search.sortBy.option.descendingDropDownOptionLabel": "{fieldName}(降順)", - "xpack.enterpriseSearch.appSearch.documents.search.sortBy.option.recentlyUploaded": "最近アップロードされたドキュメント", "xpack.enterpriseSearch.appSearch.documents.title": "ドキュメント", "xpack.enterpriseSearch.appSearch.editorRoleTypeDescription": "エディターは検索設定を管理できます。", "xpack.enterpriseSearch.appSearch.emptyState.createFirstEngineCta": "エンジンを作成", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index bbc00d8d205f7..51c4915baab29 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10300,7 +10300,6 @@ "xpack.enterpriseSearch.appSearch.documents.search.sortBy.ariaLabel": "结果排序方式", "xpack.enterpriseSearch.appSearch.documents.search.sortBy.option.ascendingDropDownOptionLabel": "{fieldName}(升序)", "xpack.enterpriseSearch.appSearch.documents.search.sortBy.option.descendingDropDownOptionLabel": "{fieldName}(降序)", - "xpack.enterpriseSearch.appSearch.documents.search.sortBy.option.recentlyUploaded": "最近上传", "xpack.enterpriseSearch.appSearch.documents.title": "文档", "xpack.enterpriseSearch.appSearch.editorRoleTypeDescription": "编辑人员可以管理搜索设置。", "xpack.enterpriseSearch.appSearch.emptyState.createFirstEngineCta": "创建引擎", From 98300c236404d0378caf26de08b0866877f997b2 Mon Sep 17 00:00:00 2001 From: Ashokaditya <1849116+ashokaditya@users.noreply.github.com> Date: Wed, 23 Mar 2022 19:39:38 +0100 Subject: [PATCH 07/13] [Security Solution][Endpoint] Accept all kinds of filenames (without wildcard) in wildcard-ed event filter `file.path.text` (#127432) * update filename regex to include multiple hyphens and periods Uses a much simpler pattern that covers a whole gamut file name patterns. fixes elastic/security-team/issues/3294 * remove duplicated code * add tests for `process.name` entry for filenames with wildcard path refs elastic/kibana/pull/120349 elastic/kibana/pull/125202 * Add file.name optimized entry when wildcard filepath in file.path.text has a filename fixes elastic/security-team/issues/3294 * update regex to include unicode chars review changes * add tests for `file.name` and `process.name` entries if it already exists This works out of the box and we don't add endpoint related `file.name` or `process.name` entry when it already is added by the user refs elastic/kibana/pull/127958#discussion_r829086447 elastic/security-team/issues/3199 * fix `file.name` and `file.path.text` entries for linux and mac/linux refs elastic/kibana/pull/127098 * do not add endpoint optimized entry Add `file.name` and `process.name` entry for wildcard path values only when file.name and process.name entries do not already exist. The earlier commit 8a516ae9c0580eb44b57666e7a5934c543c3e4bb was mistakenly labeled as this worked out of the box. In the same commit we notice that the test data had a wildcard file path that did not add a `file.name` or `process.name` entry. For more see: elastic/kibana/pull/127958#discussion_r829086447 elastic/security-team/issues/3199 * update regex to include gamut of unicode characters review suggestions * remove regex altogether simplifies the logic to check if path is without wildcard characters. This way it includes all other strings as valid filenames that do not have * or ? * update artifact creation for `file.path.text` entries Similar to when we normalize `file.path.caseless` entries, except that the `type` is `*_cased` for linux and `*_caseless` for non-linux --- .../src/path_validations/index.test.ts | 89 ++- .../src/path_validations/index.ts | 25 +- .../endpoint/lib/artifacts/lists.test.ts | 616 ++++++++++++++++++ .../server/endpoint/lib/artifacts/lists.ts | 50 +- .../manifest_manager/manifest_manager.ts | 109 ++-- 5 files changed, 790 insertions(+), 99 deletions(-) diff --git a/packages/kbn-securitysolution-utils/src/path_validations/index.test.ts b/packages/kbn-securitysolution-utils/src/path_validations/index.test.ts index ee2d8764a30af..5bb84816b1602 100644 --- a/packages/kbn-securitysolution-utils/src/path_validations/index.test.ts +++ b/packages/kbn-securitysolution-utils/src/path_validations/index.test.ts @@ -20,10 +20,31 @@ describe('validateFilePathInput', () => { describe('windows', () => { const os = OperatingSystem.WINDOWS; + it('does not warn on valid filenames', () => { + expect( + validateFilePathInput({ + os, + value: 'C:\\Windows\\*\\FILENAME.EXE-1231205124.gz', + }) + ).not.toBeDefined(); + expect( + validateFilePathInput({ + os, + value: "C:\\Windows\\*\\test$ as2@13---12!@#A,DS.#$^&$!#~ 'as'd.华语.txt", + }) + ).toEqual(undefined); + }); + it('warns on wildcard in file name at the end of the path', () => { expect(validateFilePathInput({ os, value: 'c:\\path*.exe' })).toEqual( FILENAME_WILDCARD_WARNING ); + expect( + validateFilePathInput({ + os, + value: 'C:\\Windows\\*\\FILENAME.EXE-*.gz', + }) + ).toEqual(FILENAME_WILDCARD_WARNING); }); it('warns on unix paths or non-windows paths', () => { @@ -34,6 +55,7 @@ describe('validateFilePathInput', () => { expect(validateFilePathInput({ os, value: 'c:\\path/opt' })).toEqual(FILEPATH_WARNING); expect(validateFilePathInput({ os, value: '1242' })).toEqual(FILEPATH_WARNING); expect(validateFilePathInput({ os, value: 'w12efdfa' })).toEqual(FILEPATH_WARNING); + expect(validateFilePathInput({ os, value: 'c:\\folder\\' })).toEqual(FILEPATH_WARNING); }); }); describe('unix paths', () => { @@ -42,8 +64,22 @@ describe('validateFilePathInput', () => { ? OperatingSystem.MAC : OperatingSystem.LINUX; + it('does not warn on valid filenames', () => { + expect(validateFilePathInput({ os, value: '/opt/*/FILENAME.EXE-1231205124.gz' })).not.toEqual( + FILENAME_WILDCARD_WARNING + ); + expect( + validateFilePathInput({ + os, + value: "/opt/*/test$ as2@13---12!@#A,DS.#$^&$!#~ 'as'd.华语.txt", + }) + ).not.toEqual(FILENAME_WILDCARD_WARNING); + }); it('warns on wildcard in file name at the end of the path', () => { expect(validateFilePathInput({ os, value: '/opt/bin*' })).toEqual(FILENAME_WILDCARD_WARNING); + expect(validateFilePathInput({ os, value: '/opt/FILENAME.EXE-*.gz' })).toEqual( + FILENAME_WILDCARD_WARNING + ); }); it('warns on windows paths', () => { @@ -54,6 +90,7 @@ describe('validateFilePathInput', () => { expect(validateFilePathInput({ os, value: 'opt/bin\\file.exe' })).toEqual(FILEPATH_WARNING); expect(validateFilePathInput({ os, value: '1242' })).toEqual(FILEPATH_WARNING); expect(validateFilePathInput({ os, value: 'w12efdfa' })).toEqual(FILEPATH_WARNING); + expect(validateFilePathInput({ os, value: '/folder/' })).toEqual(FILEPATH_WARNING); }); }); }); @@ -577,50 +614,82 @@ describe('Unacceptable Mac/Linux exact paths', () => { }); }); -describe('Executable filenames with wildcard PATHS', () => { +describe('hasSimpleExecutableName', () => { it('should return TRUE when MAC/LINUX wildcard paths have an executable name', () => { + const os = + parseInt((Math.random() * 2).toString(), 10) === 1 + ? OperatingSystem.MAC + : OperatingSystem.LINUX; + expect( hasSimpleExecutableName({ - os: OperatingSystem.LINUX, + os, type: 'wildcard', value: '/opt/*/app', }) ).toEqual(true); expect( hasSimpleExecutableName({ - os: OperatingSystem.MAC, + os, type: 'wildcard', value: '/op*/**/app.dmg', }) ).toEqual(true); - }); - - it('should return TRUE when WINDOWS wildcards paths have a executable name', () => { expect( hasSimpleExecutableName({ - os: OperatingSystem.WINDOWS, + os, type: 'wildcard', - value: 'c:\\**\\path.exe', + value: "/sy*/test$ as2@13---12!@#A,DS.#$^&$!#~ 'as'd.华语.txt", }) ).toEqual(true); }); it('should return FALSE when MAC/LINUX wildcard paths have a wildcard in executable name', () => { + const os = + parseInt((Math.random() * 2).toString(), 10) === 1 + ? OperatingSystem.MAC + : OperatingSystem.LINUX; + expect( hasSimpleExecutableName({ - os: OperatingSystem.LINUX, + os, type: 'wildcard', value: '/op/*/*pp', }) ).toEqual(false); expect( hasSimpleExecutableName({ - os: OperatingSystem.MAC, + os, type: 'wildcard', value: '/op*/b**/ap.m**', }) ).toEqual(false); }); + + it('should return TRUE when WINDOWS wildcards paths have a executable name', () => { + expect( + hasSimpleExecutableName({ + os: OperatingSystem.WINDOWS, + type: 'wildcard', + value: 'c:\\**\\path.exe', + }) + ).toEqual(true); + expect( + hasSimpleExecutableName({ + os: OperatingSystem.WINDOWS, + type: 'wildcard', + value: 'C:\\*\\file-name.path华语 1234.txt', + }) + ).toEqual(true); + expect( + hasSimpleExecutableName({ + os: OperatingSystem.WINDOWS, + type: 'wildcard', + value: "C:\\*\\test$ as2@13---12!@#A,DS.#$^&$!#~ 'as'd.华语.txt", + }) + ).toEqual(true); + }); + it('should return FALSE when WINDOWS wildcards paths have a wildcard in executable name', () => { expect( hasSimpleExecutableName({ diff --git a/packages/kbn-securitysolution-utils/src/path_validations/index.ts b/packages/kbn-securitysolution-utils/src/path_validations/index.ts index 97a726703feef..b64cb4cf6a052 100644 --- a/packages/kbn-securitysolution-utils/src/path_validations/index.ts +++ b/packages/kbn-securitysolution-utils/src/path_validations/index.ts @@ -31,20 +31,6 @@ export const enum OperatingSystem { export type EntryTypes = 'match' | 'wildcard' | 'match_any'; export type TrustedAppEntryTypes = Extract; -/* - * regex to match executable names - * starts matching from the eol of the path - * file names with a single or multiple spaces (for spaced names) - * and hyphens and combinations of these that produce complex names - * such as: - * c:\home\lib\dmp.dmp - * c:\home\lib\my-binary-app-+/ some/ x/ dmp.dmp - * /home/lib/dmp.dmp - * /home/lib/my-binary-app+-\ some\ x\ dmp.dmp - */ -export const WIN_EXEC_PATH = /(\\[-\w]+|\\[-\w]+[\.]+[\w]+)$/i; -export const UNIX_EXEC_PATH = /(\/[-\w]+|\/[-\w]+[\.]+[\w]+)$/i; - export const validateFilePathInput = ({ os, value = '', @@ -70,7 +56,7 @@ export const validateFilePathInput = ({ } if (isValidFilePath) { - if (!hasSimpleFileName) { + if (hasSimpleFileName !== undefined && !hasSimpleFileName) { return FILENAME_WILDCARD_WARNING; } } else { @@ -86,9 +72,14 @@ export const hasSimpleExecutableName = ({ os: OperatingSystem; type: EntryTypes; value: string; -}): boolean => { +}): boolean | undefined => { + const separator = os === OperatingSystem.WINDOWS ? '\\' : '/'; + const lastString = value.split(separator).pop(); + if (!lastString) { + return; + } if (type === 'wildcard') { - return os === OperatingSystem.WINDOWS ? WIN_EXEC_PATH.test(value) : UNIX_EXEC_PATH.test(value); + return (lastString.split('*').length || lastString.split('?').length) === 1; } return true; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts index 83dbcf1ca6f6d..179ea3827df0c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.test.ts @@ -513,6 +513,622 @@ describe('artifacts lists', () => { }); }); + describe('Endpoint Artifacts', () => { + const getOsFilter = (os: 'macos' | 'linux' | 'windows') => + `exception-list-agnostic.attributes.os_types:"${os} "`; + + describe('linux', () => { + test('it should add process.name entry when wildcard process.executable entry has filename', async () => { + const os = 'linux'; + const testEntries: EntriesArray = [ + { + field: 'process.executable.caseless', + operator: 'included', + type: 'wildcard', + value: '/usr/bi*/doc.md', + }, + ]; + + const expectedEndpointExceptions = { + type: 'simple', + entries: [ + { + field: 'process.executable', + operator: 'included', + type: 'wildcard_cased', + value: '/usr/bi*/doc.md', + }, + { + field: 'process.name', + operator: 'included', + type: 'exact_cased', + value: 'doc.md', + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + first.data[0].entries = testEntries; + first.data[0].os_types = [os]; + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + + const resp = await getFilteredEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + filter: `${getOsFilter(os)} and (exception-list-agnostic.attributes.tags:"policy:all")`, + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + }); + expect(resp).toEqual({ + entries: [expectedEndpointExceptions], + }); + }); + + test('it should add file.name entry when wildcard file.path.text entry has filename', async () => { + const os = 'linux'; + const testEntries: EntriesArray = [ + { + field: 'file.path.text', + operator: 'included', + type: 'wildcard', + value: '/usr/bi*/doc.md', + }, + ]; + + const expectedEndpointExceptions = { + type: 'simple', + entries: [ + { + field: 'file.path', + operator: 'included', + type: 'wildcard_cased', + value: '/usr/bi*/doc.md', + }, + { + field: 'file.name', + operator: 'included', + type: 'exact_cased', + value: 'doc.md', + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + first.data[0].entries = testEntries; + first.data[0].os_types = [os]; + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + + const resp = await getFilteredEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + filter: `${getOsFilter(os)} and (exception-list-agnostic.attributes.tags:"policy:all")`, + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + }); + expect(resp).toEqual({ + entries: [expectedEndpointExceptions], + }); + }); + + test('it should not add process.name entry when wildcard process.executable entry has wildcard filename', async () => { + const os = 'linux'; + const testEntries: EntriesArray = [ + { + field: 'process.executable.caseless', + operator: 'included', + type: 'wildcard', + value: '/usr/bin/*.md', + }, + ]; + + const expectedEndpointExceptions = { + type: 'simple', + entries: [ + { + field: 'process.executable', + operator: 'included', + type: 'wildcard_cased', + value: '/usr/bin/*.md', + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + first.data[0].entries = testEntries; + first.data[0].os_types = [os]; + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + + const resp = await getFilteredEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + filter: `${getOsFilter(os)} and (exception-list-agnostic.attributes.tags:"policy:all")`, + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + }); + expect(resp).toEqual({ + entries: [expectedEndpointExceptions], + }); + }); + + test('it should not add process.name entry when process.name entry already exists', async () => { + const os = 'linux'; + const testEntries: EntriesArray = [ + { + field: 'process.executable.caseless', + operator: 'included', + type: 'wildcard', + value: '/usr/bi*/donotadd.md', + }, + { + field: 'process.name', + operator: 'included', + type: 'match', + value: 'appname.exe', + }, + { + field: 'process.name', + operator: 'included', + type: 'match_any', + value: ['one.exe', 'two.exe'], + }, + ]; + + const expectedEndpointExceptions = { + type: 'simple', + entries: [ + { + field: 'process.executable', + operator: 'included', + type: 'wildcard_cased', + value: '/usr/bi*/donotadd.md', + }, + { + field: 'process.name', + operator: 'included', + type: 'exact_cased', + value: 'appname.exe', + }, + { + field: 'process.name', + operator: 'included', + type: 'exact_cased_any', + value: ['one.exe', 'two.exe'], + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + first.data[0].entries = testEntries; + first.data[0].os_types = [os]; + + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + + const resp = await getFilteredEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + filter: `${getOsFilter(os)} and (exception-list-agnostic.attributes.tags:"policy:all")`, + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + }); + + expect(resp).toEqual({ + entries: [expectedEndpointExceptions], + }); + }); + + test('it should not add file.name entry when wildcard file.path.text entry has wildcard filename', async () => { + const os = 'linux'; + const testEntries: EntriesArray = [ + { + field: 'file.path.text', + operator: 'included', + type: 'wildcard', + value: '/usr/bin/*.md', + }, + ]; + + const expectedEndpointExceptions = { + type: 'simple', + entries: [ + { + field: 'file.path', + operator: 'included', + type: 'wildcard_cased', + value: '/usr/bin/*.md', + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + first.data[0].entries = testEntries; + first.data[0].os_types = [os]; + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + + const resp = await getFilteredEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + filter: `${getOsFilter(os)} and (exception-list-agnostic.attributes.tags:"policy:all")`, + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + }); + expect(resp).toEqual({ + entries: [expectedEndpointExceptions], + }); + }); + + test('it should not add file.name entry when file.name entry already exists', async () => { + const os = 'linux'; + const testEntries: EntriesArray = [ + { + field: 'file.path.text', + operator: 'included', + type: 'wildcard', + value: '/usr/b*/donotadd.md', + }, + { + field: 'file.name', + operator: 'included', + type: 'match', + value: 'filename.app', + }, + { + field: 'file.name', + operator: 'included', + type: 'match_any', + value: ['one.app', 'two.app'], + }, + ]; + + const expectedEndpointExceptions = { + type: 'simple', + entries: [ + { + field: 'file.path', + operator: 'included', + type: 'wildcard_cased', + value: '/usr/b*/donotadd.md', + }, + { + field: 'file.name', + operator: 'included', + type: 'exact_cased', + value: 'filename.app', + }, + { + field: 'file.name', + operator: 'included', + type: 'exact_cased_any', + value: ['one.app', 'two.app'], + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + first.data[0].entries = testEntries; + first.data[0].os_types = [os]; + + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + + const resp = await getFilteredEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + filter: `${getOsFilter(os)} and (exception-list-agnostic.attributes.tags:"policy:all")`, + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + }); + + expect(resp).toEqual({ + entries: [expectedEndpointExceptions], + }); + }); + }); + + describe('macos/windows', () => { + test('it should add process.name entry for process.executable entry with wildcard type', async () => { + const os = Math.floor(Math.random() * 2) === 0 ? 'windows' : 'macos'; + const value = os === 'windows' ? 'C:\\My Doc*\\doc.md' : '/usr/bi*/doc.md'; + + const testEntries: EntriesArray = [ + { + field: 'process.executable.caseless', + operator: 'included', + type: 'wildcard', + value, + }, + ]; + + const expectedEndpointExceptions = { + type: 'simple', + entries: [ + { + field: 'process.executable', + operator: 'included', + type: 'wildcard_caseless', + value, + }, + { + field: 'process.name', + operator: 'included', + type: 'exact_caseless', + value: 'doc.md', + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + first.data[0].entries = testEntries; + first.data[0].os_types = [os]; + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + + const resp = await getFilteredEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + filter: `${getOsFilter(os)} and (exception-list-agnostic.attributes.tags:"policy:all")`, + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + }); + expect(resp).toEqual({ + entries: [expectedEndpointExceptions], + }); + }); + + test('it should add file.name entry when wildcard file.path.text entry has filename', async () => { + const os = Math.floor(Math.random() * 2) === 0 ? 'windows' : 'macos'; + const value = os === 'windows' ? 'C:\\My Doc*\\doc.md' : '/usr/bi*/doc.md'; + + const testEntries: EntriesArray = [ + { + field: 'file.path.text', + operator: 'included', + type: 'wildcard', + value, + }, + ]; + + const expectedEndpointExceptions = { + type: 'simple', + entries: [ + { + field: 'file.path', + operator: 'included', + type: 'wildcard_caseless', + value, + }, + { + field: 'file.name', + operator: 'included', + type: 'exact_caseless', + value: 'doc.md', + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + first.data[0].entries = testEntries; + first.data[0].os_types = [os]; + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + + const resp = await getFilteredEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + filter: `${getOsFilter(os)} and (exception-list-agnostic.attributes.tags:"policy:all")`, + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + }); + expect(resp).toEqual({ + entries: [expectedEndpointExceptions], + }); + }); + + test('it should not add process.name entry when wildcard process.executable entry has wildcard filename', async () => { + const os = Math.floor(Math.random() * 2) === 0 ? 'windows' : 'macos'; + const value = os === 'windows' ? 'C:\\My Doc*\\*.md' : '/usr/bin/*.md'; + + const testEntries: EntriesArray = [ + { + field: 'process.executable.caseless', + operator: 'included', + type: 'wildcard', + value, + }, + ]; + + const expectedEndpointExceptions = { + type: 'simple', + entries: [ + { + field: 'process.executable', + operator: 'included', + type: 'wildcard_caseless', + value, + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + first.data[0].entries = testEntries; + first.data[0].os_types = [os]; + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + + const resp = await getFilteredEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + filter: `${getOsFilter(os)} and (exception-list-agnostic.attributes.tags:"policy:all")`, + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + }); + expect(resp).toEqual({ + entries: [expectedEndpointExceptions], + }); + }); + + test('it should not add process.name entry when process.name entry already exists', async () => { + const os = Math.floor(Math.random() * 2) === 0 ? 'windows' : 'macos'; + const value = os === 'windows' ? 'C:\\My Doc*\\donotadd.md' : '/usr/bin/donotadd.md'; + + const testEntries: EntriesArray = [ + { + field: 'process.executable.caseless', + operator: 'included', + type: 'wildcard', + value, + }, + { + field: 'process.name', + operator: 'included', + type: 'match', + value: 'appname.exe', + }, + { + field: 'process.name', + operator: 'included', + type: 'match_any', + value: ['one.exe', 'two.exe'], + }, + ]; + + const expectedEndpointExceptions = { + type: 'simple', + entries: [ + { + field: 'process.executable', + operator: 'included', + type: 'wildcard_caseless', + value, + }, + { + field: 'process.name', + operator: 'included', + type: 'exact_caseless', + value: 'appname.exe', + }, + { + field: 'process.name', + operator: 'included', + type: 'exact_caseless_any', + value: ['one.exe', 'two.exe'], + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + first.data[0].entries = testEntries; + first.data[0].os_types = [os]; + + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + + const resp = await getFilteredEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + filter: `${getOsFilter(os)} and (exception-list-agnostic.attributes.tags:"policy:all")`, + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + }); + + expect(resp).toEqual({ + entries: [expectedEndpointExceptions], + }); + }); + + test('it should not add file.name entry when wildcard file.path.text entry has wildcard filename', async () => { + const os = Math.floor(Math.random() * 2) === 0 ? 'windows' : 'macos'; + const value = os === 'windows' ? 'C:\\My Doc*\\*.md' : '/usr/bin/*.md'; + const testEntries: EntriesArray = [ + { + field: 'file.path.text', + operator: 'included', + type: 'wildcard', + value, + }, + ]; + + const expectedEndpointExceptions = { + type: 'simple', + entries: [ + { + field: 'file.path', + operator: 'included', + type: 'wildcard_caseless', + value, + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + first.data[0].entries = testEntries; + first.data[0].os_types = [os]; + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + + const resp = await getFilteredEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + filter: `${getOsFilter(os)} and (exception-list-agnostic.attributes.tags:"policy:all")`, + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + }); + expect(resp).toEqual({ + entries: [expectedEndpointExceptions], + }); + }); + + test('it should not add file.name entry when file.name entry already exists', async () => { + const os = Math.floor(Math.random() * 2) === 0 ? 'windows' : 'macos'; + const value = os === 'windows' ? 'C:\\My Doc*\\donotadd.md' : '/usr/bin/donotadd.md'; + + const testEntries: EntriesArray = [ + { + field: 'file.path.text', + operator: 'included', + type: 'wildcard', + value, + }, + { + field: 'file.name', + operator: 'included', + type: 'match', + value: 'filename.app', + }, + { + field: 'file.name', + operator: 'included', + type: 'match_any', + value: ['one.app', 'two.app'], + }, + ]; + + const expectedEndpointExceptions = { + type: 'simple', + entries: [ + { + field: 'file.path', + operator: 'included', + type: 'wildcard_caseless', + value, + }, + { + field: 'file.name', + operator: 'included', + type: 'exact_caseless', + value: 'filename.app', + }, + { + field: 'file.name', + operator: 'included', + type: 'exact_caseless_any', + value: ['one.app', 'two.app'], + }, + ], + }; + + const first = getFoundExceptionListItemSchemaMock(); + first.data[0].entries = testEntries; + first.data[0].os_types = [os]; + + mockExceptionClient.findExceptionListItem = jest.fn().mockReturnValueOnce(first); + + const resp = await getFilteredEndpointExceptionList({ + elClient: mockExceptionClient, + schemaVersion: 'v1', + filter: `${getOsFilter(os)} and (exception-list-agnostic.attributes.tags:"policy:all")`, + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + }); + + expect(resp).toEqual({ + entries: [expectedEndpointExceptions], + }); + }); + }); + }); + const TEST_EXCEPTION_LIST_ITEM = { entries: [ { diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts index 7521ccbf9df91..2ea52485e625b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts @@ -187,11 +187,16 @@ function getMatcherFunction({ matchAny?: boolean; os: ExceptionListItemSchema['os_types'][number]; }): TranslatedEntryMatcher { + const doesFieldEndWith: boolean = + field.endsWith('.caseless') || field.endsWith('.name') || field.endsWith('.text'); + return matchAny - ? field.endsWith('.caseless') && os !== 'linux' - ? 'exact_caseless_any' + ? doesFieldEndWith + ? os === 'linux' + ? 'exact_cased_any' + : 'exact_caseless_any' : 'exact_cased_any' - : field.endsWith('.caseless') + : doesFieldEndWith ? os === 'linux' ? 'exact_cased' : 'exact_caseless' @@ -213,7 +218,9 @@ function getMatcherWildcardFunction({ } function normalizeFieldName(field: string): string { - return field.endsWith('.caseless') ? field.substring(0, field.lastIndexOf('.')) : field; + return field.endsWith('.caseless') || field.endsWith('.text') + ? field.substring(0, field.lastIndexOf('.')) + : field; } function translateItem( @@ -223,7 +230,7 @@ function translateItem( const itemSet = new Set(); const getEntries = (): TranslatedExceptionListItem['entries'] => { return item.entries.reduce((translatedEntries, entry) => { - const translatedEntry = translateEntry(schemaVersion, entry, item.os_types[0]); + const translatedEntry = translateEntry(schemaVersion, item.entries, entry, item.os_types[0]); if (translatedEntry !== undefined) { if (translatedEntryType.is(translatedEntry)) { @@ -256,12 +263,11 @@ function translateItem( }; } -function appendProcessNameEntry({ - wildcardProcessEntry, +function appendOptimizedEntryForEndpoint({ entry, os, + wildcardProcessEntry, }: { - wildcardProcessEntry: TranslatedEntryMatchWildcard; entry: { field: string; operator: 'excluded' | 'included'; @@ -269,11 +275,15 @@ function appendProcessNameEntry({ value: string; }; os: ExceptionListItemSchema['os_types'][number]; + wildcardProcessEntry: TranslatedEntryMatchWildcard; }): TranslatedPerformantEntries { const entries: TranslatedPerformantEntries = [ wildcardProcessEntry, { - field: normalizeFieldName('process.name'), + field: + entry.field === 'file.path.text' + ? normalizeFieldName('file.name') + : normalizeFieldName('process.name'), operator: entry.operator, type: (os === 'linux' ? 'exact_cased' : 'exact_caseless') as Extract< TranslatedEntryMatcher, @@ -291,6 +301,7 @@ function appendProcessNameEntry({ function translateEntry( schemaVersion: string, + exceptionListItemEntries: ExceptionListItemSchema['entries'], entry: Entry | EntryNested, os: ExceptionListItemSchema['os_types'][number] ): TranslatedEntry | TranslatedPerformantEntries | undefined { @@ -298,7 +309,12 @@ function translateEntry( case 'nested': { const nestedEntries = entry.entries.reduce( (entries, nestedEntry) => { - const translatedEntry = translateEntry(schemaVersion, nestedEntry, os); + const translatedEntry = translateEntry( + schemaVersion, + exceptionListItemEntries, + nestedEntry, + os + ); if (nestedEntry !== undefined && translatedEntryNestedEntry.is(translatedEntry)) { entries.push(translatedEntry); } @@ -354,11 +370,21 @@ function translateEntry( type: entry.type, value: entry.value, }); - if (hasExecutableName) { + + const existingFields = exceptionListItemEntries.map((e) => e.field); + const doAddPerformantEntries = !( + existingFields.includes('process.name') || existingFields.includes('file.name') + ); + + if (hasExecutableName && doAddPerformantEntries) { // when path has a full executable name // append a process.name entry based on os // `exact_cased` for linux and `exact_caseless` for others - return appendProcessNameEntry({ entry, os, wildcardProcessEntry }); + return appendOptimizedEntryForEndpoint({ + entry, + os, + wildcardProcessEntry, + }); } else { return wildcardProcessEntry; } diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts index 7be2a36396a71..a8c63bbb88e13 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts @@ -31,6 +31,7 @@ import { getArtifactId, getEndpointExceptionList, Manifest, + ArtifactListId, } from '../../../lib/artifacts'; import { InternalArtifactCompleteSchema, @@ -48,6 +49,11 @@ interface ArtifactsBuildResult { policySpecificArtifacts: Record; } +interface BuildArtifactsForOsOptions { + listId: ArtifactListId; + name: string; +} + const iterateArtifactsBuildResult = async ( result: ArtifactsBuildResult, callback: (artifact: InternalArtifactCompleteSchema, policyId?: string) => Promise @@ -174,20 +180,29 @@ export class ManifestManager { /** * Builds an artifact (one per supported OS) based on the current state of the - * Trusted Apps list (which uses the `exception-list-agnostic` SO type) + * artifacts list (Trusted Apps, Host Iso. Exceptions, Event Filters, Blocklists) + * (which uses the `exception-list-agnostic` SO type) */ - protected async buildTrustedAppsArtifact(os: string, policyId?: string) { + protected async buildArtifactsForOs({ + listId, + name, + os, + policyId, + }: { + os: string; + policyId?: string; + } & BuildArtifactsForOsOptions): Promise { return buildArtifact( await getEndpointExceptionList({ elClient: this.exceptionListClient, schemaVersion: this.schemaVersion, os, policyId, - listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + listId, }), this.schemaVersion, os, - ArtifactConstants.GLOBAL_TRUSTED_APPS_NAME + name ); } @@ -198,9 +213,13 @@ export class ManifestManager { protected async buildTrustedAppsArtifacts(): Promise { const defaultArtifacts: InternalArtifactCompleteSchema[] = []; const policySpecificArtifacts: Record = {}; + const buildArtifactsForOsOptions: BuildArtifactsForOsOptions = { + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, + name: ArtifactConstants.GLOBAL_TRUSTED_APPS_NAME, + }; for (const os of ArtifactConstants.SUPPORTED_TRUSTED_APPS_OPERATING_SYSTEMS) { - defaultArtifacts.push(await this.buildTrustedAppsArtifact(os)); + defaultArtifacts.push(await this.buildArtifactsForOs({ os, ...buildArtifactsForOsOptions })); } await iterateAllListItems( @@ -208,7 +227,9 @@ export class ManifestManager { async (policyId) => { for (const os of ArtifactConstants.SUPPORTED_TRUSTED_APPS_OPERATING_SYSTEMS) { policySpecificArtifacts[policyId] = policySpecificArtifacts[policyId] || []; - policySpecificArtifacts[policyId].push(await this.buildTrustedAppsArtifact(os, policyId)); + policySpecificArtifacts[policyId].push( + await this.buildArtifactsForOs({ os, policyId, ...buildArtifactsForOsOptions }) + ); } } ); @@ -224,9 +245,13 @@ export class ManifestManager { protected async buildEventFiltersArtifacts(): Promise { const defaultArtifacts: InternalArtifactCompleteSchema[] = []; const policySpecificArtifacts: Record = {}; + const buildArtifactsForOsOptions: BuildArtifactsForOsOptions = { + listId: ENDPOINT_EVENT_FILTERS_LIST_ID, + name: ArtifactConstants.GLOBAL_EVENT_FILTERS_NAME, + }; for (const os of ArtifactConstants.SUPPORTED_EVENT_FILTERS_OPERATING_SYSTEMS) { - defaultArtifacts.push(await this.buildEventFiltersForOs(os)); + defaultArtifacts.push(await this.buildArtifactsForOs({ os, ...buildArtifactsForOsOptions })); } await iterateAllListItems( @@ -234,7 +259,9 @@ export class ManifestManager { async (policyId) => { for (const os of ArtifactConstants.SUPPORTED_EVENT_FILTERS_OPERATING_SYSTEMS) { policySpecificArtifacts[policyId] = policySpecificArtifacts[policyId] || []; - policySpecificArtifacts[policyId].push(await this.buildEventFiltersForOs(os, policyId)); + policySpecificArtifacts[policyId].push( + await this.buildArtifactsForOs({ os, policyId, ...buildArtifactsForOsOptions }) + ); } } ); @@ -242,21 +269,6 @@ export class ManifestManager { return { defaultArtifacts, policySpecificArtifacts }; } - protected async buildEventFiltersForOs(os: string, policyId?: string) { - return buildArtifact( - await getEndpointExceptionList({ - elClient: this.exceptionListClient, - schemaVersion: this.schemaVersion, - os, - policyId, - listId: ENDPOINT_EVENT_FILTERS_LIST_ID, - }), - this.schemaVersion, - os, - ArtifactConstants.GLOBAL_EVENT_FILTERS_NAME - ); - } - /** * Builds an array of Blocklist entries (one per supported OS) based on the current state of the * Blocklist list @@ -265,9 +277,13 @@ export class ManifestManager { protected async buildBlocklistArtifacts(): Promise { const defaultArtifacts: InternalArtifactCompleteSchema[] = []; const policySpecificArtifacts: Record = {}; + const buildArtifactsForOsOptions: BuildArtifactsForOsOptions = { + listId: ENDPOINT_BLOCKLISTS_LIST_ID, + name: ArtifactConstants.GLOBAL_BLOCKLISTS_NAME, + }; for (const os of ArtifactConstants.SUPPORTED_EVENT_FILTERS_OPERATING_SYSTEMS) { - defaultArtifacts.push(await this.buildBlocklistForOs(os)); + defaultArtifacts.push(await this.buildArtifactsForOs({ os, ...buildArtifactsForOsOptions })); } await iterateAllListItems( @@ -275,7 +291,9 @@ export class ManifestManager { async (policyId) => { for (const os of ArtifactConstants.SUPPORTED_EVENT_FILTERS_OPERATING_SYSTEMS) { policySpecificArtifacts[policyId] = policySpecificArtifacts[policyId] || []; - policySpecificArtifacts[policyId].push(await this.buildBlocklistForOs(os, policyId)); + policySpecificArtifacts[policyId].push( + await this.buildArtifactsForOs({ os, policyId, ...buildArtifactsForOsOptions }) + ); } } ); @@ -283,21 +301,6 @@ export class ManifestManager { return { defaultArtifacts, policySpecificArtifacts }; } - protected async buildBlocklistForOs(os: string, policyId?: string) { - return buildArtifact( - await getEndpointExceptionList({ - elClient: this.exceptionListClient, - schemaVersion: this.schemaVersion, - os, - policyId, - listId: ENDPOINT_BLOCKLISTS_LIST_ID, - }), - this.schemaVersion, - os, - ArtifactConstants.GLOBAL_BLOCKLISTS_NAME - ); - } - /** * Builds an array of endpoint host isolation exception (one per supported OS) based on the current state of the * Host Isolation Exception List @@ -307,9 +310,13 @@ export class ManifestManager { protected async buildHostIsolationExceptionsArtifacts(): Promise { const defaultArtifacts: InternalArtifactCompleteSchema[] = []; const policySpecificArtifacts: Record = {}; + const buildArtifactsForOsOptions: BuildArtifactsForOsOptions = { + listId: ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID, + name: ArtifactConstants.GLOBAL_HOST_ISOLATION_EXCEPTIONS_NAME, + }; for (const os of ArtifactConstants.SUPPORTED_HOST_ISOLATION_EXCEPTIONS_OPERATING_SYSTEMS) { - defaultArtifacts.push(await this.buildHostIsolationExceptionForOs(os)); + defaultArtifacts.push(await this.buildArtifactsForOs({ os, ...buildArtifactsForOsOptions })); } await iterateAllListItems( @@ -318,7 +325,7 @@ export class ManifestManager { for (const os of ArtifactConstants.SUPPORTED_HOST_ISOLATION_EXCEPTIONS_OPERATING_SYSTEMS) { policySpecificArtifacts[policyId] = policySpecificArtifacts[policyId] || []; policySpecificArtifacts[policyId].push( - await this.buildHostIsolationExceptionForOs(os, policyId) + await this.buildArtifactsForOs({ os, policyId, ...buildArtifactsForOsOptions }) ); } } @@ -327,24 +334,6 @@ export class ManifestManager { return { defaultArtifacts, policySpecificArtifacts }; } - protected async buildHostIsolationExceptionForOs( - os: string, - policyId?: string - ): Promise { - return buildArtifact( - await getEndpointExceptionList({ - elClient: this.exceptionListClient, - schemaVersion: this.schemaVersion, - os, - policyId, - listId: ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID, - }), - this.schemaVersion, - os, - ArtifactConstants.GLOBAL_HOST_ISOLATION_EXCEPTIONS_NAME - ); - } - /** * Writes new artifact SO. * From 5e73ef53277aae2da5e94b10d1fe6138a4721db1 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Wed, 23 Mar 2022 12:41:27 -0600 Subject: [PATCH 08/13] [Security Solution] Collapse KPI and Table queries on Explore pages (#127930) --- .../__snapshots__/index.test.tsx.snap | 28 +- .../components/header_section/index.test.tsx | 90 +++++ .../components/header_section/index.tsx | 133 ++++--- .../matrix_histogram/index.test.tsx | 89 ++++- .../components/matrix_histogram/index.tsx | 45 ++- .../matrix_histogram/matrix_loader.tsx | 2 +- .../ml/anomaly/use_anomalies_table_data.ts | 4 +- .../ml/tables/anomalies_host_table.test.tsx | 88 +++++ .../ml/tables/anomalies_host_table.tsx | 40 +- .../tables/anomalies_network_table.test.tsx | 90 +++++ .../ml/tables/anomalies_network_table.tsx | 41 +- .../ml/tables/anomalies_user_table.test.tsx | 89 +++++ .../ml/tables/anomalies_user_table.tsx | 39 +- .../__snapshots__/index.test.tsx.snap | 3 + .../components/paginated_table/index.test.tsx | 365 ++++-------------- .../components/paginated_table/index.tsx | 113 +++--- .../components/stat_items/index.test.tsx | 198 ++++++---- .../common/components/stat_items/index.tsx | 262 +++++++------ .../containers/matrix_histogram/index.test.ts | 13 +- .../containers/matrix_histogram/index.ts | 8 + .../containers/query_toggle/index.test.tsx | 74 ++++ .../common/containers/query_toggle/index.tsx | 55 +++ .../containers/query_toggle/translations.tsx | 17 + .../use_search_strategy/index.test.ts | 17 +- .../containers/use_search_strategy/index.tsx | 8 + .../alerts_count_panel/index.test.tsx | 42 ++ .../alerts_kpis/alerts_count_panel/index.tsx | 37 +- .../alerts_histogram_panel/index.test.tsx | 46 +++ .../alerts_histogram_panel/index.tsx | 53 ++- .../alerts_kpis/common/components.tsx | 14 +- .../alerts/use_query.test.tsx | 18 + .../detection_engine/alerts/use_query.tsx | 6 + .../__snapshots__/index.test.tsx.snap | 1 + .../authentications_table/index.test.tsx | 1 + .../authentications_table/index.tsx | 3 + .../host_risk_score_table/index.tsx | 3 + .../__snapshots__/index.test.tsx.snap | 1 + .../components/hosts_table/index.test.tsx | 4 + .../hosts/components/hosts_table/index.tsx | 3 + .../kpi_hosts/authentications/index.test.tsx | 66 ++++ .../kpi_hosts/authentications/index.tsx | 13 +- .../components/kpi_hosts/common/index.tsx | 21 +- .../components/kpi_hosts/hosts/index.test.tsx | 66 ++++ .../components/kpi_hosts/hosts/index.tsx | 13 +- .../kpi_hosts/risky_hosts/index.tsx | 9 +- .../kpi_hosts/unique_ips/index.test.tsx | 66 ++++ .../components/kpi_hosts/unique_ips/index.tsx | 13 +- .../index.test.tsx | 96 ++++- .../top_host_score_contributors/index.tsx | 62 ++- .../__snapshots__/index.test.tsx.snap | 1 + .../uncommon_process_table/index.test.tsx | 121 ++---- .../uncommon_process_table/index.tsx | 3 + .../containers/authentications/index.test.tsx | 30 ++ .../containers/authentications/index.tsx | 10 +- .../hosts/containers/hosts/index.test.tsx | 30 ++ .../public/hosts/containers/hosts/index.tsx | 8 + .../kpi_hosts/authentications/index.test.tsx | 28 ++ .../kpi_hosts/authentications/index.tsx | 10 +- .../containers/kpi_hosts/hosts/index.test.tsx | 28 ++ .../containers/kpi_hosts/hosts/index.tsx | 10 +- .../hosts/containers/kpi_hosts/index.tsx | 10 - .../kpi_hosts/unique_ips/index.test.tsx | 28 ++ .../containers/kpi_hosts/unique_ips/index.tsx | 10 +- .../uncommon_processes/index.test.tsx | 30 ++ .../containers/uncommon_processes/index.tsx | 10 +- .../authentications_query_tab_body.test.tsx | 68 ++++ .../authentications_query_tab_body.tsx | 11 +- .../host_risk_score_tab_body.test.tsx | 81 ++++ .../navigation/host_risk_score_tab_body.tsx | 13 +- .../navigation/hosts_query_tab_body.test.tsx | 68 ++++ .../pages/navigation/hosts_query_tab_body.tsx | 21 +- .../uncommon_process_query_tab_body.test.tsx | 68 ++++ .../uncommon_process_query_tab_body.tsx | 13 +- .../__snapshots__/embeddable.test.tsx.snap | 1 + .../components/embeddables/embeddable.tsx | 2 +- .../embeddables/embedded_map.test.tsx | 10 +- .../components/embeddables/embedded_map.tsx | 39 +- .../components/kpi_network/dns/index.test.tsx | 66 ++++ .../components/kpi_network/dns/index.tsx | 13 +- .../network/components/kpi_network/mock.ts | 2 + .../kpi_network/network_events/index.test.tsx | 66 ++++ .../kpi_network/network_events/index.tsx | 14 +- .../kpi_network/tls_handshakes/index.test.tsx | 66 ++++ .../kpi_network/tls_handshakes/index.tsx | 13 +- .../kpi_network/unique_flows/index.test.tsx | 66 ++++ .../kpi_network/unique_flows/index.tsx | 13 +- .../unique_private_ips/index.test.tsx | 66 ++++ .../kpi_network/unique_private_ips/index.tsx | 16 +- .../__snapshots__/index.test.tsx.snap | 1 + .../network_dns_table/index.test.tsx | 37 +- .../components/network_dns_table/index.tsx | 3 + .../__snapshots__/index.test.tsx.snap | 1 + .../network_http_table/index.test.tsx | 36 +- .../components/network_http_table/index.tsx | 3 + .../__snapshots__/index.test.tsx.snap | 2 + .../index.test.tsx | 72 +--- .../network_top_countries_table/index.tsx | 3 + .../__snapshots__/index.test.tsx.snap | 2 + .../network_top_n_flow_table/index.test.tsx | 52 +-- .../network_top_n_flow_table/index.tsx | 3 + .../components/tls_table/index.test.tsx | 37 +- .../network/components/tls_table/index.tsx | 3 + .../components/users_table/index.test.tsx | 40 +- .../network/components/users_table/index.tsx | 3 + .../containers/kpi_network/dns/index.test.tsx | 28 ++ .../containers/kpi_network/dns/index.tsx | 10 +- .../network/containers/kpi_network/index.tsx | 12 - .../kpi_network/network_events/index.test.tsx | 28 ++ .../kpi_network/network_events/index.tsx | 10 +- .../kpi_network/tls_handshakes/index.test.tsx | 28 ++ .../kpi_network/tls_handshakes/index.tsx | 10 +- .../kpi_network/unique_flows/index.test.tsx | 28 ++ .../kpi_network/unique_flows/index.tsx | 11 +- .../unique_private_ips/index.test.tsx | 28 ++ .../kpi_network/unique_private_ips/index.tsx | 10 +- .../containers/network_dns/index.test.tsx | 31 ++ .../network/containers/network_dns/index.tsx | 10 +- .../containers/network_http/index.test.tsx | 31 ++ .../network/containers/network_http/index.tsx | 14 +- .../network_top_countries/index.test.tsx | 33 ++ .../network_top_countries/index.tsx | 10 +- .../network_top_n_flow/index.test.tsx | 33 ++ .../containers/network_top_n_flow/index.tsx | 10 +- .../network/containers/tls/index.test.tsx | 34 ++ .../public/network/containers/tls/index.tsx | 10 +- .../network/containers/users/index.test.tsx | 34 ++ .../public/network/containers/users/index.tsx | 10 +- .../details/network_http_query_table.tsx | 13 +- .../network_top_countries_query_table.tsx | 13 +- .../network_top_n_flow_query_table.tsx | 13 +- .../network/pages/details/tls_query_table.tsx | 13 +- .../pages/details/users_query_table.tsx | 13 +- .../navigation/countries_query_tab_body.tsx | 13 +- .../pages/navigation/dns_query_tab_body.tsx | 13 +- .../pages/navigation/http_query_tab_body.tsx | 13 +- .../pages/navigation/ips_query_tab_body.tsx | 13 +- .../pages/navigation/tls_query_tab_body.tsx | 13 +- .../components/overview_host/index.test.tsx | 29 +- .../components/overview_host/index.tsx | 43 ++- .../overview_network/index.test.tsx | 29 +- .../components/overview_network/index.tsx | 43 ++- .../containers/overview_host/index.test.tsx | 28 ++ .../containers/overview_host/index.tsx | 7 + .../overview_network/index.test.tsx | 28 ++ .../containers/overview_network/index.tsx | 7 + .../risk_score/containers/all/index.tsx | 8 + .../kpi_users/total_users/index.test.tsx | 68 ++++ .../kpi_users/total_users/index.tsx | 14 +- .../user_risk_score_table/index.test.tsx | 5 +- .../user_risk_score_table/index.tsx | 3 + .../all_users_query_tab_body.test.tsx | 68 ++++ .../navigation/all_users_query_tab_body.tsx | 13 +- .../user_risk_score_tab_body.test.tsx | 81 ++++ .../navigation/user_risk_score_tab_body.tsx | 13 +- 154 files changed, 3965 insertions(+), 1144 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_user_table.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/containers/query_toggle/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/containers/query_toggle/index.tsx create mode 100644 x-pack/plugins/security_solution/public/common/containers/query_toggle/translations.tsx create mode 100644 x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/unique_ips/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/hosts/containers/authentications/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/hosts/containers/hosts/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/index.tsx create mode 100644 x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.test.tsx create mode 100644 x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_score_tab_body.test.tsx create mode 100644 x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.test.tsx create mode 100644 x-pack/plugins/security_solution/public/hosts/pages/navigation/uncommon_process_query_tab_body.test.tsx create mode 100644 x-pack/plugins/security_solution/public/network/components/kpi_network/dns/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/network/components/kpi_network/network_events/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/network/components/kpi_network/tls_handshakes/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/network/components/kpi_network/unique_flows/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/network/components/kpi_network/unique_private_ips/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/network/containers/kpi_network/index.tsx create mode 100644 x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/network/containers/network_dns/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/network/containers/network_http/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/network/containers/tls/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/network/containers/users/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/overview/containers/overview_host/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/overview/containers/overview_network/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/users/components/kpi_users/total_users/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/users/pages/navigation/all_users_query_tab_body.test.tsx create mode 100644 x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_score_tab_body.test.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/header_section/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/header_section/__snapshots__/index.test.tsx.snap index 6701224289e66..45a6e20cf087d 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_section/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/header_section/__snapshots__/index.test.tsx.snap @@ -18,19 +18,25 @@ exports[`HeaderSection it renders 1`] = ` responsive={false} > - -

- + - Test title - -

-
+

+ + Test title + +

+ +
+ diff --git a/x-pack/plugins/security_solution/public/common/components/header_section/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/header_section/index.test.tsx index 5ec97ea59bc1d..2296dc78241f2 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_section/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_section/index.test.tsx @@ -180,4 +180,94 @@ describe('HeaderSection', () => { expect(wrapper.find('[data-test-subj="inspect-icon-button"]').first().exists()).toBe(false); }); + + test('it does not render query-toggle-header when no arguments provided', () => { + const wrapper = mount( + + +

{'Test children'}

+
+
+ ); + + expect(wrapper.find('[data-test-subj="query-toggle-header"]').first().exists()).toBe(false); + }); + + test('it does render query-toggle-header when toggleQuery arguments provided', () => { + const wrapper = mount( + + +

{'Test children'}

+
+
+ ); + + expect(wrapper.find('[data-test-subj="query-toggle-header"]').first().exists()).toBe(true); + }); + + test('it does render everything but title when toggleStatus = true', () => { + const wrapper = mount( + + +

{'Test children'}

+
+
+ ); + + expect(wrapper.find('[data-test-subj="query-toggle-header"]').first().prop('iconType')).toBe( + 'arrowDown' + ); + expect(wrapper.find('[data-test-subj="header-section-supplements"]').first().exists()).toBe( + true + ); + expect(wrapper.find('[data-test-subj="header-section-subtitle"]').first().exists()).toBe(true); + expect(wrapper.find('[data-test-subj="header-section-filters"]').first().exists()).toBe(true); + expect(wrapper.find('[data-test-subj="inspect-icon-button"]').first().exists()).toBe(true); + }); + test('it does not render anything but title when toggleStatus = false', () => { + const wrapper = mount( + + +

{'Test children'}

+
+
+ ); + + expect(wrapper.find('[data-test-subj="query-toggle-header"]').first().prop('iconType')).toBe( + 'arrowRight' + ); + expect(wrapper.find('[data-test-subj="header-section-supplements"]').first().exists()).toBe( + false + ); + expect(wrapper.find('[data-test-subj="header-section-filters"]').first().exists()).toBe(false); + expect(wrapper.find('[data-test-subj="header-section-subtitle"]').first().exists()).toBe(false); + expect(wrapper.find('[data-test-subj="inspect-icon-button"]').first().exists()).toBe(false); + }); + + test('it toggles query when icon is clicked', () => { + const mockToggle = jest.fn(); + const wrapper = mount( + + +

{'Test children'}

+
+
+ ); + wrapper.find('[data-test-subj="query-toggle-header"]').first().simulate('click'); + expect(mockToggle).toBeCalledWith(false); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/header_section/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_section/index.tsx index ae07a03ba6407..7997dfa83e27b 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_section/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_section/index.tsx @@ -5,13 +5,21 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiTitle, EuiTitleSize } from '@elastic/eui'; -import React from 'react'; +import { + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiIconTip, + EuiTitle, + EuiTitleSize, +} from '@elastic/eui'; +import React, { useCallback } from 'react'; import styled, { css } from 'styled-components'; import { InspectButton } from '../inspect'; import { Subtitle } from '../subtitle'; +import * as i18n from '../../containers/query_toggle/translations'; interface HeaderProps { border?: boolean; @@ -51,6 +59,8 @@ export interface HeaderSectionProps extends HeaderProps { split?: boolean; stackHeader?: boolean; subtitle?: string | React.ReactNode; + toggleQuery?: (status: boolean) => void; + toggleStatus?: boolean; title: string | React.ReactNode; titleSize?: EuiTitleSize; tooltip?: string; @@ -72,56 +82,87 @@ const HeaderSectionComponent: React.FC = ({ subtitle, title, titleSize = 'm', + toggleQuery, + toggleStatus = true, tooltip, -}) => ( -
- - - - - -

- {title} - {tooltip && ( - <> - {' '} - - +}) => { + const toggle = useCallback(() => { + if (toggleQuery) { + toggleQuery(!toggleStatus); + } + }, [toggleQuery, toggleStatus]); + return ( +
+ + + + + + {toggleQuery && ( + + + )} -

-
+ + +

+ {title} + {tooltip && ( + <> + {' '} + + + )} +

+
+
+
- {!hideSubtitle && ( - - )} -
- - {id && showInspectButton && ( - - + {!hideSubtitle && toggleStatus && ( + + )} - )} - {headerFilters && {headerFilters}} -
- + {id && showInspectButton && toggleStatus && ( + + + + )} - {children && ( - - {children} + {headerFilters && toggleStatus && ( + + {headerFilters} + + )} + - )} - -
-); + + {children && toggleStatus && ( + + {children} + + )} + + + ); +}; export const HeaderSection = React.memo(HeaderSectionComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx index aee49bd1b00ae..1de9e08b4c65c 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.test.tsx @@ -15,6 +15,9 @@ import { TestProviders } from '../../mock'; import { mockRuntimeMappings } from '../../containers/source/mock'; import { dnsTopDomainsLensAttributes } from '../visualization_actions/lens_attributes/network/dns_top_domains'; import { useRouteSpy } from '../../utils/route/use_route_spy'; +import { useQueryToggle } from '../../containers/query_toggle'; + +jest.mock('../../containers/query_toggle'); jest.mock('../../lib/kibana'); jest.mock('./matrix_loader', () => ({ @@ -25,9 +28,7 @@ jest.mock('../charts/barchart', () => ({ BarChart: () =>
, })); -jest.mock('../../containers/matrix_histogram', () => ({ - useMatrixHistogramCombined: jest.fn(), -})); +jest.mock('../../containers/matrix_histogram'); jest.mock('../visualization_actions', () => ({ VisualizationActions: jest.fn(({ className }: { className: string }) => ( @@ -78,9 +79,13 @@ describe('Matrix Histogram Component', () => { title: 'mockTitle', runtimeMappings: mockRuntimeMappings, }; - - beforeAll(() => { - (useMatrixHistogramCombined as jest.Mock).mockReturnValue([ + const mockUseMatrix = useMatrixHistogramCombined as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const mockSetToggle = jest.fn(); + beforeEach(() => { + jest.clearAllMocks(); + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle }); + mockUseMatrix.mockReturnValue([ false, { data: null, @@ -88,14 +93,16 @@ describe('Matrix Histogram Component', () => { totalCount: null, }, ]); - wrapper = mount(, { - wrappingComponent: TestProviders, - }); }); describe('on initial load', () => { + beforeEach(() => { + wrapper = mount(, { + wrappingComponent: TestProviders, + }); + }); test('it requests Matrix Histogram', () => { - expect(useMatrixHistogramCombined).toHaveBeenCalledWith({ + expect(mockUseMatrix).toHaveBeenCalledWith({ endDate: mockMatrixOverTimeHistogramProps.endDate, errorMessage: mockMatrixOverTimeHistogramProps.errorMessage, histogramType: mockMatrixOverTimeHistogramProps.histogramType, @@ -114,6 +121,9 @@ describe('Matrix Histogram Component', () => { describe('spacer', () => { test('it renders a spacer by default', () => { + wrapper = mount(, { + wrappingComponent: TestProviders, + }); expect(wrapper.find('[data-test-subj="spacer"]').exists()).toBe(true); }); @@ -129,8 +139,11 @@ describe('Matrix Histogram Component', () => { }); describe('not initial load', () => { - beforeAll(() => { - (useMatrixHistogramCombined as jest.Mock).mockReturnValue([ + beforeEach(() => { + wrapper = mount(, { + wrappingComponent: TestProviders, + }); + mockUseMatrix.mockReturnValue([ false, { data: [ @@ -159,6 +172,9 @@ describe('Matrix Histogram Component', () => { describe('select dropdown', () => { test('should be hidden if only one option is provided', () => { + wrapper = mount(, { + wrappingComponent: TestProviders, + }); expect(wrapper.find('EuiSelect').exists()).toBe(false); }); }); @@ -287,4 +303,53 @@ describe('Matrix Histogram Component', () => { expect(wrapper.find('[data-test-subj="mock-viz-actions"]').exists()).toBe(false); }); }); + + describe('toggle query', () => { + const testProps = { + ...mockMatrixOverTimeHistogramProps, + lensAttributes: dnsTopDomainsLensAttributes, + }; + + test('toggleQuery updates toggleStatus', () => { + wrapper = mount(, { + wrappingComponent: TestProviders, + }); + expect(mockUseMatrix.mock.calls[0][0].skip).toEqual(false); + wrapper.find('[data-test-subj="query-toggle-header"]').first().simulate('click'); + expect(mockSetToggle).toBeCalledWith(false); + expect(mockUseMatrix.mock.calls[1][0].skip).toEqual(true); + }); + + test('toggleStatus=true, do not skip', () => { + wrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect(mockUseMatrix.mock.calls[0][0].skip).toEqual(false); + }); + + test('toggleStatus=true, render components', () => { + wrapper = mount(, { + wrappingComponent: TestProviders, + }); + expect(wrapper.find('MatrixLoader').exists()).toBe(true); + }); + + test('toggleStatus=false, do not render components', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle }); + wrapper = mount(, { + wrappingComponent: TestProviders, + }); + expect(wrapper.find('MatrixLoader').exists()).toBe(false); + }); + + test('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle }); + wrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect(mockUseMatrix.mock.calls[0][0].skip).toEqual(true); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx index dbf525f8e14cb..488948de074f6 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/index.tsx @@ -34,6 +34,7 @@ import { GetLensAttributes, LensAttributes } from '../visualization_actions/type import { useKibana, useGetUserCasesPermissions } from '../../lib/kibana'; import { APP_ID, SecurityPageName } from '../../../../common/constants'; import { useRouteSpy } from '../../utils/route/use_route_spy'; +import { useQueryToggle } from '../../containers/query_toggle'; export type MatrixHistogramComponentProps = MatrixHistogramProps & Omit & { @@ -148,6 +149,19 @@ export const MatrixHistogramComponent: React.FC = }, [defaultStackByOption, stackByOptions] ); + const { toggleStatus, setToggleStatus } = useQueryToggle(id); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); + const toggleQuery = useCallback( + (status: boolean) => { + setToggleStatus(status); + // toggle on = skipQuery false + setQuerySkip(!status); + }, + [setQuerySkip, setToggleStatus] + ); const matrixHistogramRequest = { endDate, @@ -161,9 +175,8 @@ export const MatrixHistogramComponent: React.FC = runtimeMappings, isPtrIncluded, docValueFields, - skip, + skip: querySkip, }; - const [loading, { data, inspect, totalCount, refetch }] = useMatrixHistogramCombined(matrixHistogramRequest); const [{ pageName }] = useRouteSpy(); @@ -225,7 +238,7 @@ export const MatrixHistogramComponent: React.FC = > {loading && !isInitialLoading && ( @@ -239,8 +252,11 @@ export const MatrixHistogramComponent: React.FC = = {headerChildren} - - {isInitialLoading ? ( - - ) : ( - - )} + {toggleStatus ? ( + isInitialLoading ? ( + + ) : ( + + ) + ) : null} {showSpacer && } diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/matrix_loader.tsx b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/matrix_loader.tsx index efa4ba4c6eb0f..8eca508a4b74b 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/matrix_loader.tsx +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/matrix_loader.tsx @@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; import styled from 'styled-components'; const StyledEuiFlexGroup = styled(EuiFlexGroup)` - flex 1; + flex: 1; `; const MatrixLoaderComponent = () => ( diff --git a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts index f1cab9c2f441d..58610298d4395 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/anomaly/use_anomalies_table_data.ts @@ -80,7 +80,9 @@ export const useAnomaliesTableData = ({ earliestMs: number, latestMs: number ) { - if (isMlUser && !skip && jobIds.length > 0) { + if (skip) { + setLoading(false); + } else if (isMlUser && !skip && jobIds.length > 0) { try { const data = await anomaliesTableData( { diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.test.tsx new file mode 100644 index 0000000000000..7701880bd7b2e --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.test.tsx @@ -0,0 +1,88 @@ +/* + * 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 { mount } from 'enzyme'; +import { AnomaliesHostTable } from './anomalies_host_table'; +import { TestProviders } from '../../../mock'; +import React from 'react'; +import { useQueryToggle } from '../../../containers/query_toggle'; +import { useAnomaliesTableData } from '../anomaly/use_anomalies_table_data'; +import { HostsType } from '../../../../hosts/store/model'; +import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions'; + +jest.mock('../../../containers/query_toggle'); +jest.mock('../anomaly/use_anomalies_table_data'); +jest.mock('../../../../../common/machine_learning/has_ml_user_permissions'); + +describe('Anomalies host table', () => { + describe('toggle query', () => { + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const mockUseAnomaliesTableData = useAnomaliesTableData as jest.Mock; + const mockSetToggle = jest.fn(); + const testProps = { + startDate: '2019-07-17T20:00:00.000Z', + endDate: '2019-07-18T20:00:00.000Z', + narrowDateRange: jest.fn(), + skip: false, + type: HostsType.page, + }; + beforeEach(() => { + jest.clearAllMocks(); + (hasMlUserPermissions as jest.Mock).mockReturnValue(true); + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle }); + mockUseAnomaliesTableData.mockReturnValue([ + false, + { + anomalies: [], + interval: '10', + }, + ]); + }); + + test('toggleQuery updates toggleStatus', () => { + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); + expect(mockUseAnomaliesTableData.mock.calls[0][0].skip).toEqual(false); + wrapper.find('[data-test-subj="query-toggle-header"]').first().simulate('click'); + expect(mockSetToggle).toBeCalledWith(false); + expect(mockUseAnomaliesTableData.mock.calls[1][0].skip).toEqual(true); + }); + + test('toggleStatus=true, do not skip', () => { + mount(, { + wrappingComponent: TestProviders, + }); + + expect(mockUseAnomaliesTableData.mock.calls[0][0].skip).toEqual(false); + }); + + test('toggleStatus=true, render components', () => { + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); + expect(wrapper.find('[data-test-subj="host-anomalies-table"]').exists()).toBe(true); + }); + + test('toggleStatus=false, do not render components', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle }); + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); + expect(wrapper.find('[data-test-subj="host-anomalies-table"]').exists()).toBe(false); + }); + + test('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle }); + mount(, { + wrappingComponent: TestProviders, + }); + + expect(mockUseAnomaliesTableData.mock.calls[0][0].skip).toEqual(true); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx index 318f452e0c1df..eec90e6117c28 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_host_table.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { useAnomaliesTableData } from '../anomaly/use_anomalies_table_data'; import { HeaderSection } from '../../header_section'; @@ -21,6 +21,7 @@ import { BasicTable } from './basic_table'; import { getCriteriaFromHostType } from '../criteria/get_criteria_from_host_type'; import { Panel } from '../../panel'; import { anomaliesTableDefaultEquality } from './default_equality'; +import { useQueryToggle } from '../../../containers/query_toggle'; const sorting = { sort: { @@ -37,10 +38,24 @@ const AnomaliesHostTableComponent: React.FC = ({ type, }) => { const capabilities = useMlCapabilities(); + const { toggleStatus, setToggleStatus } = useQueryToggle(`AnomaliesHostTable`); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); + const toggleQuery = useCallback( + (status: boolean) => { + setToggleStatus(status); + // toggle on = skipQuery false + setQuerySkip(!status); + }, + [setQuerySkip, setToggleStatus] + ); + const [loading, tableData] = useAnomaliesTableData({ startDate, endDate, - skip, + skip: querySkip, criteriaFields: getCriteriaFromHostType(type, hostName), filterQuery: { exists: { field: 'host.name' }, @@ -64,21 +79,26 @@ const AnomaliesHostTableComponent: React.FC = ({ return ( - - type is not as specific as EUI's... - columns={columns} - items={hosts} - pagination={pagination} - sorting={sorting} - /> + {toggleStatus && ( + type is not as specific as EUI's... + columns={columns} + items={hosts} + pagination={pagination} + sorting={sorting} + /> + )} {loading && ( diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.test.tsx new file mode 100644 index 0000000000000..b7491562a5d72 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.test.tsx @@ -0,0 +1,90 @@ +/* + * 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 { mount } from 'enzyme'; +import { AnomaliesNetworkTable } from './anomalies_network_table'; +import { TestProviders } from '../../../mock'; +import React from 'react'; +import { useQueryToggle } from '../../../containers/query_toggle'; +import { useAnomaliesTableData } from '../anomaly/use_anomalies_table_data'; +import { NetworkType } from '../../../../network/store/model'; +import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions'; +import { FlowTarget } from '../../../../../common/search_strategy'; + +jest.mock('../../../containers/query_toggle'); +jest.mock('../anomaly/use_anomalies_table_data'); +jest.mock('../../../../../common/machine_learning/has_ml_user_permissions'); + +describe('Anomalies network table', () => { + describe('toggle query', () => { + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const mockUseAnomaliesTableData = useAnomaliesTableData as jest.Mock; + const mockSetToggle = jest.fn(); + const testProps = { + startDate: '2019-07-17T20:00:00.000Z', + endDate: '2019-07-18T20:00:00.000Z', + flowTarget: FlowTarget.destination, + narrowDateRange: jest.fn(), + skip: false, + type: NetworkType.page, + }; + beforeEach(() => { + jest.clearAllMocks(); + (hasMlUserPermissions as jest.Mock).mockReturnValue(true); + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle }); + mockUseAnomaliesTableData.mockReturnValue([ + false, + { + anomalies: [], + interval: '10', + }, + ]); + }); + + test('toggleQuery updates toggleStatus', () => { + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); + expect(mockUseAnomaliesTableData.mock.calls[0][0].skip).toEqual(false); + wrapper.find('[data-test-subj="query-toggle-header"]').first().simulate('click'); + expect(mockSetToggle).toBeCalledWith(false); + expect(mockUseAnomaliesTableData.mock.calls[1][0].skip).toEqual(true); + }); + + test('toggleStatus=true, do not skip', () => { + mount(, { + wrappingComponent: TestProviders, + }); + + expect(mockUseAnomaliesTableData.mock.calls[0][0].skip).toEqual(false); + }); + + test('toggleStatus=true, render components', () => { + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); + expect(wrapper.find('[data-test-subj="network-anomalies-table"]').exists()).toBe(true); + }); + + test('toggleStatus=false, do not render components', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle }); + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); + expect(wrapper.find('[data-test-subj="network-anomalies-table"]').exists()).toBe(false); + }); + + test('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle }); + mount(, { + wrappingComponent: TestProviders, + }); + + expect(mockUseAnomaliesTableData.mock.calls[0][0].skip).toEqual(true); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx index 78795c6d3614a..242114a806ca8 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_network_table.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { useAnomaliesTableData } from '../anomaly/use_anomalies_table_data'; import { HeaderSection } from '../../header_section'; @@ -20,6 +20,7 @@ import { BasicTable } from './basic_table'; import { networkEquality } from './network_equality'; import { getCriteriaFromNetworkType } from '../criteria/get_criteria_from_network_type'; import { Panel } from '../../panel'; +import { useQueryToggle } from '../../../containers/query_toggle'; const sorting = { sort: { @@ -37,10 +38,25 @@ const AnomaliesNetworkTableComponent: React.FC = ({ flowTarget, }) => { const capabilities = useMlCapabilities(); + + const { toggleStatus, setToggleStatus } = useQueryToggle(`AnomaliesNetwork-${flowTarget}`); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); + const toggleQuery = useCallback( + (status: boolean) => { + setToggleStatus(status); + // toggle on = skipQuery false + setQuerySkip(!status); + }, + [setQuerySkip, setToggleStatus] + ); + const [loading, tableData] = useAnomaliesTableData({ startDate, endDate, - skip, + skip: querySkip, criteriaFields: getCriteriaFromNetworkType(type, ip, flowTarget), }); @@ -63,18 +79,23 @@ const AnomaliesNetworkTableComponent: React.FC = ({ subtitle={`${i18n.SHOWING}: ${pagination.totalItemCount.toLocaleString()} ${i18n.UNIT( pagination.totalItemCount )}`} + height={!toggleStatus ? 40 : undefined} title={i18n.ANOMALIES} tooltip={i18n.TOOLTIP} + toggleQuery={toggleQuery} + toggleStatus={toggleStatus} isInspectDisabled={skip} /> - - type is not as specific as EUI's... - columns={columns} - items={networks} - pagination={pagination} - sorting={sorting} - /> + {toggleStatus && ( + type is not as specific as EUI's... + columns={columns} + items={networks} + pagination={pagination} + sorting={sorting} + /> + )} {loading && ( diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_user_table.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_user_table.test.tsx new file mode 100644 index 0000000000000..40aab638b854a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_user_table.test.tsx @@ -0,0 +1,89 @@ +/* + * 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 { mount } from 'enzyme'; +import { AnomaliesUserTable } from './anomalies_user_table'; +import { TestProviders } from '../../../mock'; +import React from 'react'; +import { useQueryToggle } from '../../../containers/query_toggle'; +import { useAnomaliesTableData } from '../anomaly/use_anomalies_table_data'; +import { UsersType } from '../../../../users/store/model'; +import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions'; + +jest.mock('../../../containers/query_toggle'); +jest.mock('../anomaly/use_anomalies_table_data'); +jest.mock('../../../../../common/machine_learning/has_ml_user_permissions'); + +describe('Anomalies user table', () => { + describe('toggle query', () => { + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const mockUseAnomaliesTableData = useAnomaliesTableData as jest.Mock; + const mockSetToggle = jest.fn(); + const testProps = { + startDate: '2019-07-17T20:00:00.000Z', + endDate: '2019-07-18T20:00:00.000Z', + narrowDateRange: jest.fn(), + userName: 'coolguy', + skip: false, + type: UsersType.page, + }; + beforeEach(() => { + jest.clearAllMocks(); + (hasMlUserPermissions as jest.Mock).mockReturnValue(true); + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle }); + mockUseAnomaliesTableData.mockReturnValue([ + false, + { + anomalies: [], + interval: '10', + }, + ]); + }); + + test('toggleQuery updates toggleStatus', () => { + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); + expect(mockUseAnomaliesTableData.mock.calls[0][0].skip).toEqual(false); + wrapper.find('[data-test-subj="query-toggle-header"]').first().simulate('click'); + expect(mockSetToggle).toBeCalledWith(false); + expect(mockUseAnomaliesTableData.mock.calls[1][0].skip).toEqual(true); + }); + + test('toggleStatus=true, do not skip', () => { + mount(, { + wrappingComponent: TestProviders, + }); + + expect(mockUseAnomaliesTableData.mock.calls[0][0].skip).toEqual(false); + }); + + test('toggleStatus=true, render components', () => { + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); + expect(wrapper.find('[data-test-subj="user-anomalies-table"]').exists()).toBe(true); + }); + + test('toggleStatus=false, do not render components', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle }); + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); + expect(wrapper.find('[data-test-subj="user-anomalies-table"]').exists()).toBe(false); + }); + + test('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle }); + mount(, { + wrappingComponent: TestProviders, + }); + + expect(mockUseAnomaliesTableData.mock.calls[0][0].skip).toEqual(true); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_user_table.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_user_table.tsx index 061f2c04cef6d..c67455c0772b9 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_user_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/anomalies_user_table.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { useAnomaliesTableData } from '../anomaly/use_anomalies_table_data'; import { HeaderSection } from '../../header_section'; @@ -23,6 +23,7 @@ import { Panel } from '../../panel'; import { anomaliesTableDefaultEquality } from './default_equality'; import { convertAnomaliesToUsers } from './convert_anomalies_to_users'; import { getAnomaliesUserTableColumnsCurated } from './get_anomalies_user_table_columns'; +import { useQueryToggle } from '../../../containers/query_toggle'; const sorting = { sort: { @@ -40,10 +41,24 @@ const AnomaliesUserTableComponent: React.FC = ({ }) => { const capabilities = useMlCapabilities(); + const { toggleStatus, setToggleStatus } = useQueryToggle(`AnomaliesUserTable`); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); + const toggleQuery = useCallback( + (status: boolean) => { + setToggleStatus(status); + // toggle on = skipQuery false + setQuerySkip(!status); + }, + [setQuerySkip, setToggleStatus] + ); + const [loading, tableData] = useAnomaliesTableData({ startDate, endDate, - skip, + skip: querySkip, criteriaFields: getCriteriaFromUsersType(type, userName), filterQuery: { exists: { field: 'user.name' }, @@ -67,21 +82,27 @@ const AnomaliesUserTableComponent: React.FC = ({ return ( - type is not as specific as EUI's... - columns={columns} - items={users} - pagination={pagination} - sorting={sorting} - /> + {toggleStatus && ( + type is not as specific as EUI's... + columns={columns} + items={users} + pagination={pagination} + sorting={sorting} + /> + )} {loading && ( diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap index a2fffc32be46d..bf03d637e8811 100644 --- a/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap @@ -11,6 +11,8 @@ exports[`Paginated Table Component rendering it renders the default load more ta

@@ -58,6 +60,7 @@ exports[`Paginated Table Component rendering it renders the default load more ta }, ] } + data-test-subj="paginated-basic-table" items={ Array [ Object { diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.test.tsx index 0c09dce9c07cb..57686126dfb10 100644 --- a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.test.tsx @@ -15,6 +15,8 @@ import { getHostsColumns, mockData, rowItems, sortedHosts } from './index.mock'; import { ThemeProvider } from 'styled-components'; import { getMockTheme } from '../../lib/kibana/kibana_react.mock'; import { Direction } from '../../../../common/search_strategy'; +import { useQueryToggle } from '../../containers/query_toggle'; +jest.mock('../../containers/query_toggle'); jest.mock('react', () => { const r = jest.requireActual('react'); @@ -36,37 +38,41 @@ const mockTheme = getMockTheme({ }); describe('Paginated Table Component', () => { - let loadPage: jest.Mock; - let updateLimitPagination: jest.Mock; - let updateActivePage: jest.Mock; + const loadPage = jest.fn(); + const updateLimitPagination = jest.fn(); + const updateActivePage = jest.fn(); + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const mockSetToggle = jest.fn(); + const mockSetQuerySkip = jest.fn(); + beforeEach(() => { - loadPage = jest.fn(); - updateLimitPagination = jest.fn(); - updateActivePage = jest.fn(); + jest.clearAllMocks(); + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle }); }); + const testProps = { + activePage: 0, + columns: getHostsColumns(), + headerCount: 1, + headerSupplement:

{'My test supplement.'}

, + headerTitle: 'Hosts', + headerTooltip: 'My test tooltip', + headerUnit: 'Test Unit', + itemsPerRow: rowItems, + limit: 1, + loading: false, + loadPage, + pageOfItems: mockData.Hosts.edges, + setQuerySkip: jest.fn(), + showMorePagesIndicator: true, + totalCount: 10, + updateActivePage, + updateLimitPagination: (limit: number) => updateLimitPagination({ limit }), + }; + describe('rendering', () => { test('it renders the default load more table', () => { - const wrapper = shallow( - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={1} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={(limit) => updateLimitPagination({ limit })} - /> - ); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); @@ -74,24 +80,7 @@ describe('Paginated Table Component', () => { test('it renders the loading panel at the beginning ', () => { const wrapper = mount( - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={1} - loading={true} - loadPage={loadPage} - pageOfItems={[]} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={(limit) => updateLimitPagination({ limit })} - /> +
); @@ -103,24 +92,7 @@ describe('Paginated Table Component', () => { test('it renders the over loading panel after data has been in the table ', () => { const wrapper = mount( - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={1} - loading={true} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={(limit) => updateLimitPagination({ limit })} - /> +
); @@ -130,24 +102,7 @@ describe('Paginated Table Component', () => { test('it renders the correct amount of pages and starts at activePage: 0', () => { const wrapper = mount( - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={1} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={(limit) => updateLimitPagination({ limit })} - /> +
); @@ -167,24 +122,7 @@ describe('Paginated Table Component', () => { test('it render popover to select new limit in table', () => { const wrapper = mount( - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={2} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={(limit) => updateLimitPagination({ limit })} - /> +
); @@ -195,24 +133,7 @@ describe('Paginated Table Component', () => { test('it will NOT render popover to select new limit in table if props itemsPerRow is empty', () => { const wrapper = mount( - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={[]} - limit={2} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={(limit) => updateLimitPagination({ limit })} - /> +
); @@ -224,24 +145,11 @@ describe('Paginated Table Component', () => { const wrapper = mount( {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} limit={2} - loading={false} - loadPage={jest.fn()} onChange={mockOnChange} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} sorting={{ direction: Direction.asc, field: 'node.host.name' }} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={(limit) => updateLimitPagination({ limit })} />
); @@ -253,22 +161,9 @@ describe('Paginated Table Component', () => { const wrapper = mount( {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} + {...testProps} limit={DEFAULT_MAX_TABLE_QUERY_SIZE} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} totalCount={DEFAULT_MAX_TABLE_QUERY_SIZE * 3} - updateActivePage={updateActivePage} - updateLimitPagination={(limit) => updateLimitPagination({ limit })} />
); @@ -279,24 +174,7 @@ describe('Paginated Table Component', () => { test('Should show items per row if totalCount is greater than items', () => { const wrapper = mount( - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={DEFAULT_MAX_TABLE_QUERY_SIZE} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={30} - updateActivePage={updateActivePage} - updateLimitPagination={(limit) => updateLimitPagination({ limit })} - /> +
); expect(wrapper.find('[data-test-subj="loadingMoreSizeRowPopover"]').exists()).toBeTruthy(); @@ -305,24 +183,7 @@ describe('Paginated Table Component', () => { test('Should hide items per row if totalCount is less than items', () => { const wrapper = mount( - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={DEFAULT_MAX_TABLE_QUERY_SIZE} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={1} - updateActivePage={updateActivePage} - updateLimitPagination={(limit) => updateLimitPagination({ limit })} - /> +
); expect(wrapper.find('[data-test-subj="loadingMoreSizeRowPopover"]').exists()).toBeFalsy(); @@ -331,24 +192,7 @@ describe('Paginated Table Component', () => { test('Should hide pagination if totalCount is zero', () => { const wrapper = mount( - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={DEFAULT_MAX_TABLE_QUERY_SIZE} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={0} - updateActivePage={updateActivePage} - updateLimitPagination={(limit) => updateLimitPagination({ limit })} - /> +
); @@ -360,24 +204,7 @@ describe('Paginated Table Component', () => { test('should call updateActivePage with 1 when clicking to the first page', () => { const wrapper = mount( - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={1} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={(limit) => updateLimitPagination({ limit })} - /> +
); wrapper.find('[data-test-subj="pagination-button-next"]').first().simulate('click'); @@ -387,24 +214,7 @@ describe('Paginated Table Component', () => { test('Should call updateActivePage with 0 when you pick a new limit', () => { const wrapper = mount( - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={2} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={(limit) => updateLimitPagination({ limit })} - /> +
); wrapper.find('[data-test-subj="pagination-button-next"]').first().simulate('click'); @@ -417,22 +227,8 @@ describe('Paginated Table Component', () => { test('should update the page when the activePage is changed from redux', () => { const ourProps: BasicTableProps = { + ...testProps, activePage: 3, - columns: getHostsColumns(), - headerCount: 1, - headerSupplement:

{'My test supplement.'}

, - headerTitle: 'Hosts', - headerTooltip: 'My test tooltip', - headerUnit: 'Test Unit', - itemsPerRow: rowItems, - limit: 1, - loading: false, - loadPage, - pageOfItems: mockData.Hosts.edges, - showMorePagesIndicator: true, - totalCount: 10, - updateActivePage, - updateLimitPagination: (limit) => updateLimitPagination({ limit }), }; // enzyme does not allow us to pass props to child of HOC @@ -462,24 +258,7 @@ describe('Paginated Table Component', () => { test('Should call updateLimitPagination when you pick a new limit', () => { const wrapper = mount( - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={2} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={(limit) => updateLimitPagination({ limit })} - /> +
); @@ -494,24 +273,11 @@ describe('Paginated Table Component', () => { const wrapper = mount( {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} limit={2} - loading={false} - loadPage={jest.fn()} onChange={mockOnChange} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} sorting={{ direction: Direction.asc, field: 'node.host.name' }} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={(limit) => updateLimitPagination({ limit })} />
); @@ -524,4 +290,41 @@ describe('Paginated Table Component', () => { ]); }); }); + + describe('Toggle query', () => { + test('toggleQuery updates toggleStatus', () => { + const wrapper = mount( + + + + ); + wrapper.find('[data-test-subj="query-toggle-header"]').first().simulate('click'); + expect(mockSetToggle).toBeCalledWith(false); + expect(mockSetQuerySkip).toBeCalledWith(true); + }); + + test('toggleStatus=true, render table', () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="paginated-basic-table"]').first().exists()).toEqual( + true + ); + }); + + test('toggleStatus=false, hide table', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle }); + + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="paginated-basic-table"]').first().exists()).toEqual( + false + ); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx index 310ab039057c2..b9de144c5735e 100644 --- a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx @@ -20,7 +20,7 @@ import { EuiTableRowCellProps, } from '@elastic/eui'; import { noop } from 'lodash/fp'; -import React, { FC, memo, useState, useMemo, useEffect, ComponentType } from 'react'; +import React, { FC, memo, useState, useMemo, useEffect, ComponentType, useCallback } from 'react'; import styled from 'styled-components'; import { Direction } from '../../../../common/search_strategy'; @@ -49,6 +49,7 @@ import { useStateToaster } from '../toasters'; import * as i18n from './translations'; import { Panel } from '../panel'; import { InspectButtonContainer } from '../inspect'; +import { useQueryToggle } from '../../containers/query_toggle'; const DEFAULT_DATA_TEST_SUBJ = 'paginated-table'; @@ -113,6 +114,7 @@ export interface BasicTableProps { onChange?: (criteria: Criteria) => void; // eslint-disable-next-line @typescript-eslint/no-explicit-any pageOfItems: any[]; + setQuerySkip: (skip: boolean) => void; showMorePagesIndicator: boolean; sorting?: SortingBasicTable; split?: boolean; @@ -153,6 +155,7 @@ const PaginatedTableComponent: FC = ({ loadPage, onChange = noop, pageOfItems, + setQuerySkip, showMorePagesIndicator, sorting = null, split, @@ -253,10 +256,24 @@ const PaginatedTableComponent: FC = ({ [sorting] ); + const { toggleStatus, setToggleStatus } = useQueryToggle(id); + + const toggleQuery = useCallback( + (status: boolean) => { + setToggleStatus(status); + // toggle on = skipQuery false + setQuerySkip(!status); + }, + [setQuerySkip, setToggleStatus] + ); + return ( = ({ > {!loadingInitial && headerSupplement} - - {loadingInitial ? ( - - ) : ( - <> - - - - {itemsPerRow && itemsPerRow.length > 0 && totalCount >= itemsPerRow[0].numberOfRow && ( - - - - )} - - - - {totalCount > 0 && ( - - )} - - - {(isInspect || myLoading) && ( - - )} - - )} + {toggleStatus && + (loadingInitial ? ( + + ) : ( + <> + + + + {itemsPerRow && + itemsPerRow.length > 0 && + totalCount >= itemsPerRow[0].numberOfRow && ( + + + + )} + + + + {totalCount > 0 && ( + + )} + + + {(isInspect || myLoading) && ( + + )} + + ))} ); diff --git a/x-pack/plugins/security_solution/public/common/components/stat_items/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/stat_items/index.test.tsx index 5f2c76632aba9..944eeb8b42a57 100644 --- a/x-pack/plugins/security_solution/public/common/components/stat_items/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/stat_items/index.test.tsx @@ -41,6 +41,7 @@ import { NetworkKpiStrategyResponse, } from '../../../../common/search_strategy'; import { getMockTheme } from '../../lib/kibana/kibana_react.mock'; +import * as module from '../../containers/query_toggle'; const from = '2019-06-15T06:00:00.000Z'; const to = '2019-06-18T06:00:00.000Z'; @@ -53,26 +54,37 @@ jest.mock('../charts/barchart', () => { return { BarChart: () =>
}; }); +const mockSetToggle = jest.fn(); + +jest + .spyOn(module, 'useQueryToggle') + .mockImplementation(() => ({ toggleStatus: true, setToggleStatus: mockSetToggle })); +const mockSetQuerySkip = jest.fn(); describe('Stat Items Component', () => { const mockTheme = getMockTheme({ eui: { euiColorMediumShade: '#ece' } }); const state: State = mockGlobalState; const { storage } = createSecuritySolutionStorageMock(); const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - + const testProps = { + description: 'HOSTS', + fields: [{ key: 'hosts', value: null, color: '#6092C0', icon: 'cross' }], + from, + id: 'statItems', + key: 'mock-keys', + loading: false, + setQuerySkip: mockSetQuerySkip, + to, + narrowDateRange: mockNarrowDateRange, + }; + beforeEach(() => { + jest.clearAllMocks(); + }); describe.each([ [ mount( - + ), @@ -81,17 +93,7 @@ describe('Stat Items Component', () => { mount( - + ), @@ -118,62 +120,59 @@ describe('Stat Items Component', () => { }); }); + const mockStatItemsData: StatItemsProps = { + ...testProps, + areaChart: [ + { + key: 'uniqueSourceIpsHistogram', + value: [ + { x: new Date('2019-05-03T13:00:00.000Z').toISOString(), y: 565975 }, + { x: new Date('2019-05-04T01:00:00.000Z').toISOString(), y: 1084366 }, + { x: new Date('2019-05-04T13:00:00.000Z').toISOString(), y: 12280 }, + ], + color: '#D36086', + }, + { + key: 'uniqueDestinationIpsHistogram', + value: [ + { x: new Date('2019-05-03T13:00:00.000Z').toISOString(), y: 565975 }, + { x: new Date('2019-05-04T01:00:00.000Z').toISOString(), y: 1084366 }, + { x: new Date('2019-05-04T13:00:00.000Z').toISOString(), y: 12280 }, + ], + color: '#9170B8', + }, + ], + barChart: [ + { key: 'uniqueSourceIps', value: [{ x: 'uniqueSourceIps', y: '1714' }], color: '#D36086' }, + { + key: 'uniqueDestinationIps', + value: [{ x: 'uniqueDestinationIps', y: 2354 }], + color: '#9170B8', + }, + ], + description: 'UNIQUE_PRIVATE_IPS', + enableAreaChart: true, + enableBarChart: true, + fields: [ + { + key: 'uniqueSourceIps', + description: 'Source', + value: 1714, + color: '#D36086', + icon: 'cross', + }, + { + key: 'uniqueDestinationIps', + description: 'Dest.', + value: 2359, + color: '#9170B8', + icon: 'cross', + }, + ], + }; + + let wrapper: ReactWrapper; describe('rendering kpis with charts', () => { - const mockStatItemsData: StatItemsProps = { - areaChart: [ - { - key: 'uniqueSourceIpsHistogram', - value: [ - { x: new Date('2019-05-03T13:00:00.000Z').toISOString(), y: 565975 }, - { x: new Date('2019-05-04T01:00:00.000Z').toISOString(), y: 1084366 }, - { x: new Date('2019-05-04T13:00:00.000Z').toISOString(), y: 12280 }, - ], - color: '#D36086', - }, - { - key: 'uniqueDestinationIpsHistogram', - value: [ - { x: new Date('2019-05-03T13:00:00.000Z').toISOString(), y: 565975 }, - { x: new Date('2019-05-04T01:00:00.000Z').toISOString(), y: 1084366 }, - { x: new Date('2019-05-04T13:00:00.000Z').toISOString(), y: 12280 }, - ], - color: '#9170B8', - }, - ], - barChart: [ - { key: 'uniqueSourceIps', value: [{ x: 'uniqueSourceIps', y: '1714' }], color: '#D36086' }, - { - key: 'uniqueDestinationIps', - value: [{ x: 'uniqueDestinationIps', y: 2354 }], - color: '#9170B8', - }, - ], - description: 'UNIQUE_PRIVATE_IPS', - enableAreaChart: true, - enableBarChart: true, - fields: [ - { - key: 'uniqueSourceIps', - description: 'Source', - value: 1714, - color: '#D36086', - icon: 'cross', - }, - { - key: 'uniqueDestinationIps', - description: 'Dest.', - value: 2359, - color: '#9170B8', - icon: 'cross', - }, - ], - from, - id: 'statItems', - key: 'mock-keys', - to, - narrowDateRange: mockNarrowDateRange, - }; - let wrapper: ReactWrapper; beforeAll(() => { wrapper = mount( @@ -202,6 +201,43 @@ describe('Stat Items Component', () => { expect(wrapper.find(EuiHorizontalRule)).toHaveLength(1); }); }); + describe('Toggle query', () => { + test('toggleQuery updates toggleStatus', () => { + wrapper = mount( + + + + ); + wrapper.find('[data-test-subj="query-toggle-stat"]').first().simulate('click'); + expect(mockSetToggle).toBeCalledWith(false); + expect(mockSetQuerySkip).toBeCalledWith(true); + }); + test('toggleStatus=true, render all', () => { + wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="inspect-icon-button"]').first().exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="stat-title"]').first().exists()).toEqual(true); + }); + test('toggleStatus=false, render none', () => { + jest + .spyOn(module, 'useQueryToggle') + .mockImplementation(() => ({ toggleStatus: false, setToggleStatus: mockSetToggle })); + wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="inspect-icon-button"]').first().exists()).toEqual( + false + ); + expect(wrapper.find('[data-test-subj="stat-title"]').first().exists()).toEqual(false); + }); + }); }); describe('addValueToFields', () => { @@ -244,7 +280,9 @@ describe('useKpiMatrixStatus', () => { 'statItem', from, to, - mockNarrowDateRange + mockNarrowDateRange, + mockSetQuerySkip, + false ); return ( @@ -262,8 +300,10 @@ describe('useKpiMatrixStatus', () => { ); - - expect(wrapper.find('MockChildComponent').get(0).props).toEqual(mockEnableChartsData); + const result = { ...wrapper.find('MockChildComponent').get(0).props }; + const { setQuerySkip, ...restResult } = result; + const { setQuerySkip: a, ...restExpect } = mockEnableChartsData; + expect(restResult).toEqual(restExpect); }); test('it should not append areaChart if enableAreaChart is off', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx b/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx index 424920d34e2e8..6de3cc07472bc 100644 --- a/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx @@ -12,13 +12,16 @@ import { EuiPanel, EuiHorizontalRule, EuiIcon, + EuiButtonIcon, + EuiLoadingSpinner, EuiTitle, IconType, } from '@elastic/eui'; import { get, getOr } from 'lodash/fp'; -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; +import { useQueryToggle } from '../../containers/query_toggle'; import { HostsKpiStrategyResponse, @@ -34,6 +37,7 @@ import { InspectButton } from '../inspect'; import { VisualizationActions, HISTOGRAM_ACTIONS_BUTTON_CLASS } from '../visualization_actions'; import { HoverVisibilityContainer } from '../hover_visibility_container'; import { LensAttributes } from '../visualization_actions/types'; +import * as i18n from '../../containers/query_toggle/translations'; import { UserskKpiStrategyResponse } from '../../../../common/search_strategy/security_solution/users'; const FlexItem = styled(EuiFlexItem)` @@ -84,6 +88,8 @@ export interface StatItemsProps extends StatItems { narrowDateRange: UpdateDateRange; to: string; showInspectButton?: boolean; + loading: boolean; + setQuerySkip: (skip: boolean) => void; } export const numberFormatter = (value: string | number): string => value.toLocaleString(); @@ -176,33 +182,27 @@ export const useKpiMatrixStatus = ( id: string, from: string, to: string, - narrowDateRange: UpdateDateRange -): StatItemsProps[] => { - const [statItemsProps, setStatItemsProps] = useState(mappings as StatItemsProps[]); - - useEffect(() => { - setStatItemsProps( - mappings.map((stat) => { - return { - ...stat, - areaChart: stat.enableAreaChart ? addValueToAreaChart(stat.fields, data) : undefined, - barChart: stat.enableBarChart ? addValueToBarChart(stat.fields, data) : undefined, - fields: addValueToFields(stat.fields, data), - id, - key: `kpi-summary-${stat.key}`, - statKey: `${stat.key}`, - from, - to, - narrowDateRange, - }; - }) - ); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [data]); - - return statItemsProps; -}; - + narrowDateRange: UpdateDateRange, + setQuerySkip: (skip: boolean) => void, + loading: boolean +): StatItemsProps[] => + mappings.map((stat) => ({ + ...stat, + areaChart: stat.enableAreaChart ? addValueToAreaChart(stat.fields, data) : undefined, + barChart: stat.enableBarChart ? addValueToBarChart(stat.fields, data) : undefined, + fields: addValueToFields(stat.fields, data), + id, + key: `kpi-summary-${stat.key}`, + statKey: `${stat.key}`, + from, + to, + narrowDateRange, + setQuerySkip, + loading, + })); +const StyledTitle = styled.h6` + line-height: 200%; +`; export const StatItemsComponent = React.memo( ({ areaChart, @@ -214,13 +214,15 @@ export const StatItemsComponent = React.memo( from, grow, id, - showInspectButton, + loading = false, + showInspectButton = true, index, narrowDateRange, statKey = 'item', to, barChartLensAttributes, areaChartLensAttributes, + setQuerySkip, }) => { const isBarChartDataAvailable = barChart && @@ -239,101 +241,143 @@ export const StatItemsComponent = React.memo( [from, to] ); + const { toggleStatus, setToggleStatus } = useQueryToggle(id); + + const toggleQuery = useCallback( + (status: boolean) => { + setToggleStatus(status); + // toggle on = skipQuery false + setQuerySkip(!status); + }, + [setQuerySkip, setToggleStatus] + ); + const toggle = useCallback(() => toggleQuery(!toggleStatus), [toggleQuery, toggleStatus]); + return ( - -
{description}
-
+ + + + + + + {description} + + +
- {showInspectButton && ( + {showInspectButton && toggleStatus && !loading && ( )}
+ {loading && ( + + + + + + )} + {toggleStatus && !loading && ( + <> + + {fields.map((field) => ( + + + {(isAreaChartDataAvailable || isBarChartDataAvailable) && field.icon && ( + + + + )} - - {fields.map((field) => ( - - - {(isAreaChartDataAvailable || isBarChartDataAvailable) && field.icon && ( - - - - )} + + + +

+ {field.value != null + ? field.value.toLocaleString() + : getEmptyTagValue()}{' '} + {field.description} +

+
+ {field.lensAttributes && timerange && ( + + )} +
+
+
+
+ ))} +
+ {(enableAreaChart || enableBarChart) && } + + {enableBarChart && ( - - -

- {field.value != null ? field.value.toLocaleString() : getEmptyTagValue()}{' '} - {field.description} -

-
- {field.lensAttributes && timerange && ( - - )} -
+
-
-
- ))} -
+ )} - {(enableAreaChart || enableBarChart) && } - - {enableBarChart && ( - - - - )} - - {enableAreaChart && from != null && to != null && ( - <> - - - - - )} - + {enableAreaChart && from != null && to != null && ( + <> + + + + + )} + + + )}
); @@ -344,6 +388,8 @@ export const StatItemsComponent = React.memo( prevProps.enableBarChart === nextProps.enableBarChart && prevProps.from === nextProps.from && prevProps.grow === nextProps.grow && + prevProps.loading === nextProps.loading && + prevProps.setQuerySkip === nextProps.setQuerySkip && prevProps.id === nextProps.id && prevProps.index === nextProps.index && prevProps.narrowDateRange === nextProps.narrowDateRange && diff --git a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.test.ts b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.test.ts index e09dbe23d512a..138fa99ef4074 100644 --- a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.test.ts +++ b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.test.ts @@ -6,7 +6,6 @@ */ import { renderHook, act } from '@testing-library/react-hooks'; - import { useKibana } from '../../../common/lib/kibana'; import { useMatrixHistogram, useMatrixHistogramCombined } from '.'; import { MatrixHistogramType } from '../../../../common/search_strategy'; @@ -39,6 +38,7 @@ describe('useMatrixHistogram', () => { indexNames: [], stackByField: 'event.module', startDate: new Date(Date.now()).toISOString(), + skip: false, }; afterEach(() => { @@ -145,6 +145,17 @@ describe('useMatrixHistogram', () => { mockDnsSearchStrategyResponse.rawResponse.aggregations?.dns_name_query_count.buckets ); }); + + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { ...props }; + const { rerender } = renderHook(() => useMatrixHistogram(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(3); + }); }); describe('useMatrixHistogramCombined', () => { diff --git a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts index c49a9d0438b2d..f6670c98fc0ee 100644 --- a/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts +++ b/x-pack/plugins/security_solution/public/common/containers/matrix_histogram/index.ts @@ -229,6 +229,14 @@ export const useMatrixHistogram = ({ }; }, [matrixHistogramRequest, hostsSearch, skip]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + const runMatrixHistogramSearch = useCallback( (to: string, from: string) => { hostsSearch({ diff --git a/x-pack/plugins/security_solution/public/common/containers/query_toggle/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/query_toggle/index.test.tsx new file mode 100644 index 0000000000000..76f1c02dcb43c --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/query_toggle/index.test.tsx @@ -0,0 +1,74 @@ +/* + * 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 { + renderHook, + act, + RenderResult, + WaitForNextUpdate, + cleanup, +} from '@testing-library/react-hooks'; +import { QueryToggle, useQueryToggle } from '.'; +import { RouteSpyState } from '../../utils/route/types'; +import { SecurityPageName } from '../../../../common/constants'; +import { useKibana } from '../../lib/kibana'; + +const mockRouteSpy: RouteSpyState = { + pageName: SecurityPageName.overview, + detailName: undefined, + tabName: undefined, + search: '', + pathName: '/', +}; +jest.mock('../../lib/kibana'); +jest.mock('../../utils/route/use_route_spy', () => ({ + useRouteSpy: () => [mockRouteSpy], +})); + +describe('useQueryToggle', () => { + let result: RenderResult; + let waitForNextUpdate: WaitForNextUpdate; + const mockSet = jest.fn(); + beforeAll(() => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + storage: { + get: () => true, + set: mockSet, + }, + }, + }); + }); + beforeEach(() => { + jest.clearAllMocks(); + }); + it('Toggles local storage', async () => { + await act(async () => { + ({ result, waitForNextUpdate } = renderHook(() => useQueryToggle('queryId'))); + await waitForNextUpdate(); + expect(result.current.toggleStatus).toEqual(true); + }); + act(() => { + result.current.setToggleStatus(false); + }); + expect(result.current.toggleStatus).toEqual(false); + expect(mockSet).toBeCalledWith('kibana.siem:queryId.query.toggle:overview', false); + cleanup(); + }); + it('null storage key, do not set', async () => { + await act(async () => { + ({ result, waitForNextUpdate } = renderHook(() => useQueryToggle())); + await waitForNextUpdate(); + expect(result.current.toggleStatus).toEqual(true); + }); + act(() => { + result.current.setToggleStatus(false); + }); + expect(mockSet).not.toBeCalled(); + cleanup(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/containers/query_toggle/index.tsx b/x-pack/plugins/security_solution/public/common/containers/query_toggle/index.tsx new file mode 100644 index 0000000000000..53bcd6b60fc1b --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/query_toggle/index.tsx @@ -0,0 +1,55 @@ +/* + * 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 { useEffect, useCallback, useState } from 'react'; +import { useKibana } from '../../lib/kibana'; +import { useRouteSpy } from '../../utils/route/use_route_spy'; + +export const getUniqueStorageKey = (pageName: string, id?: string): string | null => + id && pageName.length > 0 ? `kibana.siem:${id}.query.toggle:${pageName}` : null; +export interface QueryToggle { + toggleStatus: boolean; + setToggleStatus: (b: boolean) => void; +} + +export const useQueryToggle = (id?: string): QueryToggle => { + const [{ pageName }] = useRouteSpy(); + const { + services: { storage }, + } = useKibana(); + const storageKey = getUniqueStorageKey(pageName, id); + + const [storageValue, setStorageValue] = useState( + storageKey != null ? storage.get(storageKey) ?? true : true + ); + + useEffect(() => { + if (storageKey != null) { + setStorageValue(storage.get(storageKey) ?? true); + } + }, [storage, storageKey]); + + const setToggleStatus = useCallback( + (isOpen: boolean) => { + if (storageKey != null) { + storage.set(storageKey, isOpen); + setStorageValue(isOpen); + } + }, + [storage, storageKey] + ); + + return id + ? { + toggleStatus: storageValue, + setToggleStatus, + } + : { + toggleStatus: true, + setToggleStatus: () => {}, + }; +}; diff --git a/x-pack/plugins/security_solution/public/common/containers/query_toggle/translations.tsx b/x-pack/plugins/security_solution/public/common/containers/query_toggle/translations.tsx new file mode 100644 index 0000000000000..acb64e7e6b510 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/query_toggle/translations.tsx @@ -0,0 +1,17 @@ +/* + * 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 QUERY_BUTTON_TITLE = (buttonOn: boolean) => + buttonOn + ? i18n.translate('xpack.securitySolution.toggleQuery.on', { + defaultMessage: 'Open', + }) + : i18n.translate('xpack.securitySolution.toggleQuery.off', { + defaultMessage: 'Closed', + }); diff --git a/x-pack/plugins/security_solution/public/common/containers/use_search_strategy/index.test.ts b/x-pack/plugins/security_solution/public/common/containers/use_search_strategy/index.test.ts index 5bfa9028a0fe8..c1513b7a0485b 100644 --- a/x-pack/plugins/security_solution/public/common/containers/use_search_strategy/index.test.ts +++ b/x-pack/plugins/security_solution/public/common/containers/use_search_strategy/index.test.ts @@ -6,7 +6,7 @@ */ import { useSearchStrategy } from './index'; -import { renderHook } from '@testing-library/react-hooks'; +import { act, renderHook } from '@testing-library/react-hooks'; import { useObservable } from '@kbn/securitysolution-hook-utils'; import { FactoryQueryTypes } from '../../../../common/search_strategy'; @@ -200,4 +200,19 @@ describe('useSearchStrategy', () => { expect(start).toBeCalledWith(expect.objectContaining({ signal })); }); + it('skip = true will cancel any running request', () => { + const abortSpy = jest.fn(); + const signal = new AbortController().signal; + jest.spyOn(window, 'AbortController').mockReturnValue({ abort: abortSpy, signal }); + const factoryQueryType = 'fakeQueryType' as FactoryQueryTypes; + const localProps = { + ...userSearchStrategyProps, + skip: false, + factoryQueryType, + }; + const { rerender } = renderHook(() => useSearchStrategy(localProps)); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/containers/use_search_strategy/index.tsx b/x-pack/plugins/security_solution/public/common/containers/use_search_strategy/index.tsx index 77676a83d39b6..234cf039024ba 100644 --- a/x-pack/plugins/security_solution/public/common/containers/use_search_strategy/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/use_search_strategy/index.tsx @@ -96,6 +96,7 @@ export const useSearchStrategy = ({ factoryQueryType, initialResult, errorMessage, + skip = false, }: { factoryQueryType: QueryType; /** @@ -106,6 +107,7 @@ export const useSearchStrategy = ({ * Message displayed to the user on a Toast when an erro happens. */ errorMessage?: string; + skip?: boolean; }) => { const abortCtrl = useRef(new AbortController()); const { getTransformChangesIfTheyExist } = useTransforms(); @@ -154,6 +156,12 @@ export const useSearchStrategy = ({ }; }, []); + useEffect(() => { + if (skip) { + abortCtrl.current.abort(); + } + }, [skip]); + const [formatedResult, inspect] = useMemo( () => [ result diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.test.tsx index d0b05587a4711..4fc47421a720e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.test.tsx @@ -12,7 +12,9 @@ import { mount } from 'enzyme'; import { TestProviders } from '../../../../common/mock'; import { AlertsCountPanel } from './index'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; +jest.mock('../../../../common/containers/query_toggle'); jest.mock('react-router-dom', () => { const actual = jest.requireActual('react-router-dom'); return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) }; @@ -22,6 +24,12 @@ describe('AlertsCountPanel', () => { const defaultProps = { signalIndexName: 'signalIndexName', }; + const mockSetToggle = jest.fn(); + const mockUseQueryToggle = useQueryToggle as jest.Mock; + beforeEach(() => { + jest.clearAllMocks(); + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle }); + }); it('renders correctly', async () => { await act(async () => { @@ -54,4 +62,38 @@ describe('AlertsCountPanel', () => { }); }); }); + describe('toggleQuery', () => { + it('toggles', async () => { + await act(async () => { + const wrapper = mount( + + + + ); + wrapper.find('[data-test-subj="query-toggle-header"]').first().simulate('click'); + expect(mockSetToggle).toBeCalledWith(false); + }); + }); + it('toggleStatus=true, render', async () => { + await act(async () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="alertsCountTable"]').exists()).toEqual(true); + }); + }); + it('toggleStatus=false, hide', async () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle }); + await act(async () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="alertsCountTable"]').exists()).toEqual(false); + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx index 04b8f482fd121..1c0e2144ad9d4 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx @@ -6,7 +6,7 @@ */ import { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types'; -import React, { memo, useMemo, useState, useEffect } from 'react'; +import React, { memo, useMemo, useState, useEffect, useCallback } from 'react'; import uuid from 'uuid'; import type { Filter, Query } from '@kbn/es-query'; @@ -24,6 +24,7 @@ import type { AlertsCountAggregation } from './types'; import { DEFAULT_STACK_BY_FIELD } from '../common/config'; import { KpiPanel, StackByComboBox } from '../common/components'; import { useInspectButton } from '../common/hooks'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; export const DETECTIONS_ALERTS_COUNT_ID = 'detections-alerts-count'; @@ -64,6 +65,20 @@ export const AlertsCountPanel = memo( } }, [query, filters]); + const { toggleStatus, setToggleStatus } = useQueryToggle(DETECTIONS_ALERTS_COUNT_ID); + const [querySkip, setQuerySkip] = useState(!toggleStatus); + useEffect(() => { + setQuerySkip(!toggleStatus); + }, [toggleStatus]); + const toggleQuery = useCallback( + (status: boolean) => { + setToggleStatus(status); + // toggle on = skipQuery false + setQuerySkip(!status); + }, + [setQuerySkip, setToggleStatus] + ); + const { loading: isLoadingAlerts, data: alertsData, @@ -80,6 +95,7 @@ export const AlertsCountPanel = memo( runtimeMappings ), indexName: signalIndexName, + skip: querySkip, }); useEffect(() => { @@ -99,21 +115,26 @@ export const AlertsCountPanel = memo( }); return ( - - + + - + {toggleStatus && ( + + )} ); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx index 29e18a1c49c12..3135e2e173793 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.test.tsx @@ -12,9 +12,13 @@ import { mount } from 'enzyme'; import type { Filter } from '@kbn/es-query'; import { TestProviders } from '../../../../common/mock'; import { SecurityPageName } from '../../../../app/types'; +import { MatrixLoader } from '../../../../common/components/matrix_histogram/matrix_loader'; import { AlertsHistogramPanel } from './index'; import * as helpers from './helpers'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; + +jest.mock('../../../../common/containers/query_toggle'); jest.mock('react-router-dom', () => { const originalModule = jest.requireActual('react-router-dom'); @@ -91,6 +95,12 @@ describe('AlertsHistogramPanel', () => { updateDateRange: jest.fn(), }; + const mockSetToggle = jest.fn(); + const mockUseQueryToggle = useQueryToggle as jest.Mock; + beforeEach(() => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle }); + }); + afterEach(() => { jest.clearAllMocks(); jest.restoreAllMocks(); @@ -339,4 +349,40 @@ describe('AlertsHistogramPanel', () => { `); }); }); + + describe('toggleQuery', () => { + it('toggles', async () => { + await act(async () => { + const wrapper = mount( + + + + ); + wrapper.find('[data-test-subj="query-toggle-header"]').first().simulate('click'); + expect(mockSetToggle).toBeCalledWith(false); + }); + }); + it('toggleStatus=true, render', async () => { + await act(async () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(MatrixLoader).exists()).toEqual(true); + }); + }); + it('toggleStatus=false, hide', async () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle }); + await act(async () => { + const wrapper = mount( + + + + ); + expect(wrapper.find(MatrixLoader).exists()).toEqual(false); + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx index 571f656389f6a..84476c3ee6885 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx @@ -45,6 +45,7 @@ import type { AlertsStackByField } from '../common/types'; import { KpiPanel, StackByComboBox } from '../common/components'; import { useInspectButton } from '../common/hooks'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; const defaultTotalAlertsObj: AlertsTotal = { value: 0, @@ -116,6 +117,19 @@ export const AlertsHistogramPanel = memo( onlyField == null ? defaultStackByOption : onlyField ); + const { toggleStatus, setToggleStatus } = useQueryToggle(DETECTIONS_HISTOGRAM_ID); + const [querySkip, setQuerySkip] = useState(!toggleStatus); + useEffect(() => { + setQuerySkip(!toggleStatus); + }, [toggleStatus]); + const toggleQuery = useCallback( + (status: boolean) => { + setToggleStatus(status); + // toggle on = skipQuery false + setQuerySkip(!status); + }, + [setQuerySkip, setToggleStatus] + ); const { loading: isLoadingAlerts, data: alertsData, @@ -132,6 +146,7 @@ export const AlertsHistogramPanel = memo( runtimeMappings ), indexName: signalIndexName, + skip: querySkip, }); const kibana = useKibana(); @@ -270,17 +285,21 @@ export const AlertsHistogramPanel = memo( ); return ( - + ( - {isInitialLoading ? ( - - ) : ( - - )} + {toggleStatus ? ( + isInitialLoading ? ( + + ) : ( + + ) + ) : null} ); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/components.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/components.tsx index 6a56f7bc220ac..27f33409ae1a5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/components.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/components.tsx @@ -12,17 +12,23 @@ import { PANEL_HEIGHT, MOBILE_PANEL_HEIGHT } from './config'; import { useStackByFields } from './hooks'; import * as i18n from './translations'; -export const KpiPanel = styled(EuiPanel)<{ height?: number }>` +export const KpiPanel = styled(EuiPanel)<{ height?: number; $toggleStatus: boolean }>` display: flex; flex-direction: column; position: relative; overflow: hidden; - - height: ${MOBILE_PANEL_HEIGHT}px; - @media only screen and (min-width: ${(props) => props.theme.eui.euiBreakpoints.m}) { + ${({ $toggleStatus }) => + $toggleStatus && + ` height: ${PANEL_HEIGHT}px; + `} } + ${({ $toggleStatus }) => + $toggleStatus && + ` + height: ${MOBILE_PANEL_HEIGHT}px; + `} `; interface StackedBySelectProps { selected: string; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.test.tsx index 277e2008601dc..5ed7a219e5068 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.test.tsx @@ -129,4 +129,22 @@ describe('useQueryAlerts', () => { }); }); }); + + test('skip', async () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + await act(async () => { + const localProps = { query: mockAlertsQuery, indexName, skip: false }; + const { rerender, waitForNextUpdate } = renderHook< + [object, string], + ReturnQueryAlerts + >(() => useQueryAlerts(localProps)); + await waitForNextUpdate(); + await waitForNextUpdate(); + + localProps.skip = true; + act(() => rerender()); + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(2); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx index b2bbcdf277992..2b98987e52675 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_query.tsx @@ -94,6 +94,12 @@ export const useQueryAlerts = ({ if (!isEmpty(query) && !skip) { fetchData(); } + if (skip) { + setLoading(false); + isSubscribed = false; + abortCtrl.abort(); + } + return () => { isSubscribed = false; abortCtrl.abort(); diff --git a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/__snapshots__/index.test.tsx.snap index ed119568cdcb3..bffd5e2261ad9 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/__snapshots__/index.test.tsx.snap @@ -105,6 +105,7 @@ exports[`Authentication Table Component rendering it renders the authentication isInspect={false} loadPage={[MockFunction]} loading={false} + setQuerySkip={[MockFunction]} showMorePagesIndicator={true} totalCount={54} type="page" diff --git a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.test.tsx index 14dc1769dbd05..2ec333e335639 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.test.tsx @@ -45,6 +45,7 @@ describe('Authentication Table Component', () => { isInspect={false} loading={false} loadPage={loadPage} + setQuerySkip={jest.fn()} showMorePagesIndicator={getOr( false, 'showMorePagesIndicator', diff --git a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx index 4402f6a210947..2bbda82e15315 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx @@ -43,6 +43,7 @@ interface AuthenticationTableProps { loadPage: (newActivePage: number) => void; id: string; isInspect: boolean; + setQuerySkip: (skip: boolean) => void; showMorePagesIndicator: boolean; totalCount: number; type: hostsModel.HostsType; @@ -78,6 +79,7 @@ const AuthenticationTableComponent: React.FC = ({ isInspect, loading, loadPage, + setQuerySkip, showMorePagesIndicator, totalCount, type, @@ -133,6 +135,7 @@ const AuthenticationTableComponent: React.FC = ({ loading={loading} loadPage={loadPage} pageOfItems={data} + setQuerySkip={setQuerySkip} showMorePagesIndicator={showMorePagesIndicator} totalCount={fakeTotalCount} updateLimitPagination={updateLimitPagination} diff --git a/x-pack/plugins/security_solution/public/hosts/components/host_risk_score_table/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/host_risk_score_table/index.tsx index e4130eee21909..f4da6983fc590 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/host_risk_score_table/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/host_risk_score_table/index.tsx @@ -54,6 +54,7 @@ interface HostRiskScoreTableProps { isInspect: boolean; loading: boolean; loadPage: (newActivePage: number) => void; + setQuerySkip: (skip: boolean) => void; severityCount: SeverityCount; totalCount: number; type: hostsModel.HostsType; @@ -71,6 +72,7 @@ const HostRiskScoreTableComponent: React.FC = ({ isInspect, loading, loadPage, + setQuerySkip, severityCount, totalCount, type, @@ -207,6 +209,7 @@ const HostRiskScoreTableComponent: React.FC = ({ loadPage={loadPage} onChange={onSort} pageOfItems={data} + setQuerySkip={setQuerySkip} showMorePagesIndicator={false} sorting={sort} split={true} diff --git a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/__snapshots__/index.test.tsx.snap index 59a00cbf190f6..f646fc12c4697 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/__snapshots__/index.test.tsx.snap @@ -36,6 +36,7 @@ exports[`Hosts Table rendering it renders the default Hosts table 1`] = ` isInspect={false} loadPage={[MockFunction]} loading={false} + setQuerySkip={[MockFunction]} showMorePagesIndicator={false} totalCount={-1} type="page" diff --git a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx index 71efbb0a44d15..43dc31c68d1bc 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.test.tsx @@ -69,6 +69,7 @@ describe('Hosts Table', () => { fakeTotalCount={0} loading={false} loadPage={loadPage} + setQuerySkip={jest.fn()} showMorePagesIndicator={false} totalCount={-1} type={hostsModel.HostsType.page} @@ -91,6 +92,7 @@ describe('Hosts Table', () => { data={mockData} totalCount={0} fakeTotalCount={-1} + setQuerySkip={jest.fn()} showMorePagesIndicator={false} loadPage={loadPage} type={hostsModel.HostsType.page} @@ -113,6 +115,7 @@ describe('Hosts Table', () => { data={mockData} totalCount={0} fakeTotalCount={-1} + setQuerySkip={jest.fn()} showMorePagesIndicator={false} loadPage={loadPage} type={hostsModel.HostsType.page} @@ -136,6 +139,7 @@ describe('Hosts Table', () => { data={mockData} totalCount={0} fakeTotalCount={-1} + setQuerySkip={jest.fn()} showMorePagesIndicator={false} loadPage={loadPage} type={hostsModel.HostsType.page} diff --git a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.tsx index 01306004844d8..42c8254ffd183 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/hosts_table/index.tsx @@ -42,6 +42,7 @@ interface HostsTableProps { isInspect: boolean; loading: boolean; loadPage: (newActivePage: number) => void; + setQuerySkip: (skip: boolean) => void; showMorePagesIndicator: boolean; totalCount: number; type: hostsModel.HostsType; @@ -77,6 +78,7 @@ const HostsTableComponent: React.FC = ({ isInspect, loading, loadPage, + setQuerySkip, showMorePagesIndicator, totalCount, type, @@ -172,6 +174,7 @@ const HostsTableComponent: React.FC = ({ loadPage={loadPage} onChange={onChange} pageOfItems={data} + setQuerySkip={setQuerySkip} showMorePagesIndicator={showMorePagesIndicator} sorting={sorting} totalCount={fakeTotalCount} diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.test.tsx new file mode 100644 index 0000000000000..164b88399bbe9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.test.tsx @@ -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 { useHostsKpiAuthentications } from '../../../containers/kpi_hosts/authentications'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; +import { render } from '@testing-library/react'; +import { TestProviders } from '../../../../common/mock'; +import React from 'react'; +import { HostsKpiAuthentications } from './index'; + +jest.mock('../../../../common/containers/query_toggle'); +jest.mock('../../../containers/kpi_hosts/authentications'); +jest.mock('../common', () => ({ + KpiBaseComponentManage: () => , +})); + +describe('Authentications KPI', () => { + const mockUseHostsKpiAuthentications = useHostsKpiAuthentications as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + from: '2019-06-25T04:31:59.345Z', + to: '2019-06-25T06:31:59.345Z', + indexNames: [], + narrowDateRange: jest.fn(), + setQuery: jest.fn(), + skip: false, + }; + beforeEach(() => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseHostsKpiAuthentications.mockReturnValue([ + false, + { + id: '123', + inspect: { + dsl: [], + response: [], + }, + refetch: jest.fn(), + }, + ]); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(mockUseHostsKpiAuthentications.mock.calls[0][0].skip).toEqual(false); + }); + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + render( + + + + ); + expect(mockUseHostsKpiAuthentications.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.tsx index 1158c842e04cb..f12eca88ffc95 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.tsx @@ -5,17 +5,18 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { StatItems } from '../../../../common/components/stat_items'; import { kpiUserAuthenticationsAreaLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/hosts/kpi_user_authentications_area'; import { kpiUserAuthenticationsBarLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/hosts/kpi_user_authentications_bar'; import { kpiUserAuthenticationsMetricSuccessLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/hosts/kpi_user_authentications_metric_success'; import { kpiUserAuthenticationsMetricFailureLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/hosts/kpi_user_authentication_metric_failure'; -import { useHostsKpiAuthentications } from '../../../containers/kpi_hosts/authentications'; +import { useHostsKpiAuthentications, ID } from '../../../containers/kpi_hosts/authentications'; import { KpiBaseComponentManage } from '../common'; import { HostsKpiProps, HostsKpiChartColors } from '../types'; import * as i18n from './translations'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; export const fieldsMapping: Readonly = [ { @@ -57,12 +58,17 @@ const HostsKpiAuthenticationsComponent: React.FC = ({ setQuery, skip, }) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [loading, { refetch, id, inspect, ...data }] = useHostsKpiAuthentications({ filterQuery, endDate: to, indexNames, startDate: from, - skip, + skip: querySkip, }); return ( @@ -77,6 +83,7 @@ const HostsKpiAuthenticationsComponent: React.FC = ({ narrowDateRange={narrowDateRange} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} /> ); }; diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/common/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/common/index.tsx index e3460ec22e73e..4296ae4984b95 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/common/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/common/index.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiFlexItem, EuiLoadingSpinner, EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; @@ -42,10 +42,11 @@ interface KpiBaseComponentProps { from: string; to: string; narrowDateRange: UpdateDateRange; + setQuerySkip: (skip: boolean) => void; } export const KpiBaseComponent = React.memo( - ({ fieldsMapping, data, id, loading = false, from, to, narrowDateRange }) => { + ({ fieldsMapping, data, id, loading = false, from, to, narrowDateRange, setQuerySkip }) => { const { cases } = useKibana().services; const CasesContext = cases.ui.getCasesContext(); const userPermissions = useGetUserCasesPermissions(); @@ -57,13 +58,11 @@ export const KpiBaseComponent = React.memo( id, from, to, - narrowDateRange + narrowDateRange, + setQuerySkip, + loading ); - if (loading) { - return ; - } - return ( @@ -87,11 +86,3 @@ export const KpiBaseComponent = React.memo( KpiBaseComponent.displayName = 'KpiBaseComponent'; export const KpiBaseComponentManage = manageQuery(KpiBaseComponent); - -export const KpiBaseComponentLoader: React.FC = () => ( - - - - - -); diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/index.test.tsx new file mode 100644 index 0000000000000..49b6986515564 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/index.test.tsx @@ -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 { useHostsKpiHosts } from '../../../containers/kpi_hosts/hosts'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; +import { render } from '@testing-library/react'; +import { TestProviders } from '../../../../common/mock'; +import React from 'react'; +import { HostsKpiHosts } from './index'; + +jest.mock('../../../../common/containers/query_toggle'); +jest.mock('../../../containers/kpi_hosts/hosts'); +jest.mock('../common', () => ({ + KpiBaseComponentManage: () => , +})); + +describe('Hosts KPI', () => { + const mockUseHostsKpiHosts = useHostsKpiHosts as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + from: '2019-06-25T04:31:59.345Z', + to: '2019-06-25T06:31:59.345Z', + indexNames: [], + narrowDateRange: jest.fn(), + setQuery: jest.fn(), + skip: false, + }; + beforeEach(() => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseHostsKpiHosts.mockReturnValue([ + false, + { + id: '123', + inspect: { + dsl: [], + response: [], + }, + refetch: jest.fn(), + }, + ]); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(mockUseHostsKpiHosts.mock.calls[0][0].skip).toEqual(false); + }); + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + render( + + + + ); + expect(mockUseHostsKpiHosts.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/index.tsx index 79118b66a3f71..b29bdddd44e35 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/index.tsx @@ -5,15 +5,16 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { StatItems } from '../../../../common/components/stat_items'; import { kpiHostAreaLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/hosts/kpi_host_area'; import { kpiHostMetricLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/hosts/kpi_host_metric'; -import { useHostsKpiHosts } from '../../../containers/kpi_hosts/hosts'; +import { useHostsKpiHosts, ID } from '../../../containers/kpi_hosts/hosts'; import { KpiBaseComponentManage } from '../common'; import { HostsKpiProps, HostsKpiChartColors } from '../types'; import * as i18n from './translations'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; export const fieldsMapping: Readonly = [ { @@ -42,12 +43,17 @@ const HostsKpiHostsComponent: React.FC = ({ setQuery, skip, }) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [loading, { refetch, id, inspect, ...data }] = useHostsKpiHosts({ filterQuery, endDate: to, indexNames, startDate: from, - skip, + skip: querySkip, }); return ( @@ -62,6 +68,7 @@ const HostsKpiHostsComponent: React.FC = ({ narrowDateRange={narrowDateRange} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} /> ); }; diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.tsx index f515490252d40..0a86a9006b637 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.tsx @@ -11,6 +11,7 @@ import { EuiHorizontalRule, EuiIcon, EuiPanel, + EuiLoadingSpinner, EuiTitle, EuiText, } from '@elastic/eui'; @@ -22,7 +23,6 @@ import { BUTTON_CLASS as INPECT_BUTTON_CLASS, } from '../../../../common/components/inspect'; -import { KpiBaseComponentLoader } from '../common'; import * as i18n from './translations'; import { useInspectQuery } from '../../../../common/hooks/use_inspect_query'; @@ -36,6 +36,13 @@ import { HoverVisibilityContainer } from '../../../../common/components/hover_vi import { KpiRiskScoreStrategyResponse, RiskSeverity } from '../../../../../common/search_strategy'; import { RiskScore } from '../../../../common/components/severity/common'; +const KpiBaseComponentLoader: React.FC = () => ( + + + + + +); const QUERY_ID = 'hostsKpiRiskyHostsQuery'; const HostCount = styled(EuiText)` diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/unique_ips/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/unique_ips/index.test.tsx new file mode 100644 index 0000000000000..20de5db340b5e --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/unique_ips/index.test.tsx @@ -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 { useHostsKpiUniqueIps } from '../../../containers/kpi_hosts/unique_ips'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; +import { render } from '@testing-library/react'; +import { TestProviders } from '../../../../common/mock'; +import React from 'react'; +import { HostsKpiUniqueIps } from './index'; + +jest.mock('../../../../common/containers/query_toggle'); +jest.mock('../../../containers/kpi_hosts/unique_ips'); +jest.mock('../common', () => ({ + KpiBaseComponentManage: () => , +})); + +describe('Unique IPs KPI', () => { + const mockUseHostsKpiUniqueIps = useHostsKpiUniqueIps as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + from: '2019-06-25T04:31:59.345Z', + to: '2019-06-25T06:31:59.345Z', + indexNames: [], + narrowDateRange: jest.fn(), + setQuery: jest.fn(), + skip: false, + }; + beforeEach(() => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseHostsKpiUniqueIps.mockReturnValue([ + false, + { + id: '123', + inspect: { + dsl: [], + response: [], + }, + refetch: jest.fn(), + }, + ]); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(mockUseHostsKpiUniqueIps.mock.calls[0][0].skip).toEqual(false); + }); + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + render( + + + + ); + expect(mockUseHostsKpiUniqueIps.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/unique_ips/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/unique_ips/index.tsx index ef7bdfa1dc031..ef032d041db7d 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/unique_ips/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/unique_ips/index.tsx @@ -5,17 +5,18 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { StatItems } from '../../../../common/components/stat_items'; import { kpiUniqueIpsAreaLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_area'; import { kpiUniqueIpsBarLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_bar'; import { kpiUniqueIpsDestinationMetricLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_destination_metric'; import { kpiUniqueIpsSourceMetricLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/hosts/kpi_unique_ips_source_metric'; -import { useHostsKpiUniqueIps } from '../../../containers/kpi_hosts/unique_ips'; +import { useHostsKpiUniqueIps, ID } from '../../../containers/kpi_hosts/unique_ips'; import { KpiBaseComponentManage } from '../common'; import { HostsKpiProps, HostsKpiChartColors } from '../types'; import * as i18n from './translations'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; export const fieldsMapping: Readonly = [ { @@ -57,12 +58,17 @@ const HostsKpiUniqueIpsComponent: React.FC = ({ setQuery, skip, }) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [loading, { refetch, id, inspect, ...data }] = useHostsKpiUniqueIps({ filterQuery, endDate: to, indexNames, startDate: from, - skip, + skip: querySkip, }); return ( @@ -77,6 +83,7 @@ const HostsKpiUniqueIpsComponent: React.FC = ({ narrowDateRange={narrowDateRange} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} /> ); }; diff --git a/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.test.tsx index 2f3a414344cfc..5ff8696ae5be3 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.test.tsx @@ -5,16 +5,30 @@ * 2.0. */ -import { render } from '@testing-library/react'; +import { render, fireEvent } from '@testing-library/react'; import React from 'react'; import { TopHostScoreContributors } from '.'; import { TestProviders } from '../../../common/mock'; import { useHostRiskScore } from '../../../risk_score/containers'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; +jest.mock('../../../common/containers/query_toggle'); jest.mock('../../../risk_score/containers'); const useHostRiskScoreMock = useHostRiskScore as jest.Mock; - +const testProps = { + setQuery: jest.fn(), + deleteQuery: jest.fn(), + hostName: 'test-host-name', + from: '2020-07-07T08:20:18.966Z', + to: '2020-07-08T08:20:18.966Z', +}; describe('Host Risk Flyout', () => { + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const mockSetToggle = jest.fn(); + beforeEach(() => { + jest.clearAllMocks(); + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: mockSetToggle }); + }); it('renders', () => { useHostRiskScoreMock.mockReturnValueOnce([ true, @@ -26,13 +40,7 @@ describe('Host Risk Flyout', () => { const { queryByTestId } = render( - + ); @@ -69,13 +77,7 @@ describe('Host Risk Flyout', () => { const { queryAllByRole } = render( - + ); @@ -83,4 +85,66 @@ describe('Host Risk Flyout', () => { expect(queryAllByRole('row')[2]).toHaveTextContent('second'); expect(queryAllByRole('row')[3]).toHaveTextContent('third'); }); + + describe('toggleQuery', () => { + beforeEach(() => { + useHostRiskScoreMock.mockReturnValue([ + true, + { + data: [], + isModuleEnabled: true, + }, + ]); + }); + + test('toggleQuery updates toggleStatus', () => { + const { getByTestId } = render( + + + + ); + expect(useHostRiskScoreMock.mock.calls[0][0].skip).toEqual(false); + fireEvent.click(getByTestId('query-toggle-header')); + expect(mockSetToggle).toBeCalledWith(false); + expect(useHostRiskScoreMock.mock.calls[1][0].skip).toEqual(true); + }); + + test('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(useHostRiskScoreMock.mock.calls[0][0].skip).toEqual(false); + }); + + test('toggleStatus=true, render components', () => { + const { queryByTestId } = render( + + + + ); + expect(queryByTestId('topHostScoreContributors-table')).toBeTruthy(); + }); + + test('toggleStatus=false, do not render components', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle }); + const { queryByTestId } = render( + + + + ); + expect(queryByTestId('topHostScoreContributors-table')).toBeFalsy(); + }); + + test('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: mockSetToggle }); + render( + + + + ); + expect(useHostRiskScoreMock.mock.calls[0][0].skip).toEqual(true); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.tsx index 8811a6b64e7fc..a3b7022ee83ef 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { EuiFlexGroup, @@ -27,6 +27,7 @@ import { HostsComponentsQueryProps } from '../../pages/navigation/types'; import { RuleLink } from '../../../detections/pages/detection_engine/rules/all/use_columns'; import { HostRiskScoreQueryId, useHostRiskScore } from '../../../risk_score/containers'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; export interface TopHostScoreContributorsProps extends Pick { @@ -77,11 +78,27 @@ const TopHostScoreContributorsComponent: React.FC const sort = useMemo(() => ({ field: RiskScoreFields.timestamp, direction: Direction.desc }), []); + const { toggleStatus, setToggleStatus } = useQueryToggle(QUERY_ID); + const [querySkip, setQuerySkip] = useState(!toggleStatus); + useEffect(() => { + setQuerySkip(!toggleStatus); + }, [toggleStatus]); + + const toggleQuery = useCallback( + (status: boolean) => { + setToggleStatus(status); + // toggle on = skipQuery false + setQuerySkip(!status); + }, + [setQuerySkip, setToggleStatus] + ); + const [loading, { data, refetch, inspect }] = useHostRiskScore({ filterQuery: hostName ? buildHostNamesFilter([hostName]) : undefined, timerange, onlyLatest: false, sort, + skip: querySkip, pagination: { querySize: 1, cursorStart: 0, @@ -119,24 +136,37 @@ const TopHostScoreContributorsComponent: React.FC - - - - - - - - - - - + {toggleStatus && ( + + + + )} + + {toggleStatus && ( + + + + + + )} ); diff --git a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/__snapshots__/index.test.tsx.snap index a93c4062e8808..19a6018f6b680 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/__snapshots__/index.test.tsx.snap @@ -205,6 +205,7 @@ exports[`Uncommon Process Table Component rendering it renders the default Uncom isInspect={false} loadPage={[MockFunction]} loading={false} + setQuerySkip={[MockFunction]} showMorePagesIndicator={true} totalCount={5} type="page" diff --git a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx index 29d3f110e8181..300abc60818cb 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.test.tsx @@ -36,21 +36,24 @@ describe('Uncommon Process Table Component', () => { const loadPage = jest.fn(); const mount = useMountAppended(); + const defaultProps = { + data: mockData.edges, + fakeTotalCount: getOr(50, 'fakeTotalCount', mockData.pageInfo), + id: 'uncommonProcess', + isInspect: false, + loading: false, + loadPage, + setQuerySkip: jest.fn(), + showMorePagesIndicator: getOr(false, 'showMorePagesIndicator', mockData.pageInfo), + totalCount: mockData.totalCount, + type: hostsModel.HostsType.page, + }; + describe('rendering', () => { test('it renders the default Uncommon process table', () => { const wrapper = shallow( - + ); @@ -60,17 +63,7 @@ describe('Uncommon Process Table Component', () => { test('it has a double dash (empty value) without any hosts at all', () => { const wrapper = mount( - + ); expect(wrapper.find('.euiTableRow').at(0).find('.euiTableRowCell').at(3).text()).toBe( @@ -81,17 +74,7 @@ describe('Uncommon Process Table Component', () => { test('it has a single host without any extra comma when the number of hosts is exactly 1', () => { const wrapper = mount( - + ); @@ -103,17 +86,7 @@ describe('Uncommon Process Table Component', () => { test('it has a single link when the number of hosts is exactly 1', () => { const wrapper = mount( - + ); @@ -125,17 +98,7 @@ describe('Uncommon Process Table Component', () => { test('it has a comma separated list of hosts when the number of hosts is greater than 1', () => { const wrapper = mount( - + ); @@ -147,17 +110,7 @@ describe('Uncommon Process Table Component', () => { test('it has 2 links when the number of hosts is equal to 2', () => { const wrapper = mount( - + ); @@ -169,17 +122,7 @@ describe('Uncommon Process Table Component', () => { test('it is empty when all hosts are invalid because they do not contain an id and a name', () => { const wrapper = mount( - + ); expect(wrapper.find('.euiTableRow').at(3).find('.euiTableRowCell').at(3).text()).toBe( @@ -190,17 +133,7 @@ describe('Uncommon Process Table Component', () => { test('it has no link when all hosts are invalid because they do not contain an id and a name', () => { const wrapper = mount( - + ); expect( @@ -211,17 +144,7 @@ describe('Uncommon Process Table Component', () => { test('it is returns two hosts when others are invalid because they do not contain an id and a name', () => { const wrapper = mount( - + ); expect(wrapper.find('.euiTableRow').at(4).find('.euiTableRowCell').at(3).text()).toBe( diff --git a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.tsx index 0af27bdb0ba18..cbdae1747e5f6 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/uncommon_process_table/index.tsx @@ -30,6 +30,7 @@ interface UncommonProcessTableProps { isInspect: boolean; loading: boolean; loadPage: (newActivePage: number) => void; + setQuerySkip: (skip: boolean) => void; showMorePagesIndicator: boolean; totalCount: number; type: hostsModel.HostsType; @@ -72,6 +73,7 @@ const UncommonProcessTableComponent = React.memo( loading, loadPage, totalCount, + setQuerySkip, showMorePagesIndicator, type, }) => { @@ -125,6 +127,7 @@ const UncommonProcessTableComponent = React.memo( loading={loading} loadPage={loadPage} pageOfItems={data} + setQuerySkip={setQuerySkip} showMorePagesIndicator={showMorePagesIndicator} totalCount={fakeTotalCount} updateLimitPagination={updateLimitPagination} diff --git a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.test.tsx new file mode 100644 index 0000000000000..1f6ee4cb276ec --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.test.tsx @@ -0,0 +1,30 @@ +/* + * 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 { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../common/mock'; +import { useAuthentications } from './index'; +import { HostsType } from '../../store/model'; + +describe('authentications', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + type: HostsType.page, + skip: false, + }; + const { rerender } = renderHook(() => useAuthentications(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx index f446380e54937..1ff27e4b29917 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/authentications/index.tsx @@ -36,7 +36,7 @@ import * as i18n from './translations'; import { useTransforms } from '../../../transforms/containers/use_transforms'; import { useAppToasts } from '../../../common/hooks/use_app_toasts'; -const ID = 'hostsAuthenticationsQuery'; +export const ID = 'hostsAuthenticationsQuery'; export interface AuthenticationArgs { authentications: AuthenticationsEdges[]; @@ -215,5 +215,13 @@ export const useAuthentications = ({ }; }, [authenticationsRequest, authenticationsSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, authenticationsResponse]; }; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.test.tsx new file mode 100644 index 0000000000000..df64f4cd6f81a --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.test.tsx @@ -0,0 +1,30 @@ +/* + * 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 { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../common/mock'; +import { useAllHost } from './index'; +import { HostsType } from '../../store/model'; + +describe('useAllHost', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + type: HostsType.page, + skip: false, + }; + const { rerender } = renderHook(() => useAllHost(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx index 1a9e86755cf7d..c4259e8a5a737 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/hosts/index.tsx @@ -217,5 +217,13 @@ export const useAllHost = ({ }; }, [hostsRequest, hostsSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, hostsResponse]; }; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.test.tsx new file mode 100644 index 0000000000000..f62fc3a77786e --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.test.tsx @@ -0,0 +1,28 @@ +/* + * 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 { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../../common/mock'; +import { useHostsKpiAuthentications } from './index'; + +describe('kpi hosts - authentications', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + skip: false, + }; + const { rerender } = renderHook(() => useHostsKpiAuthentications(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx index c15c68d246f14..9fa38c14e2ea4 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx @@ -26,7 +26,7 @@ import * as i18n from './translations'; import { getInspectResponse } from '../../../../helpers'; import { InspectResponse } from '../../../../types'; -const ID = 'hostsKpiAuthenticationsQuery'; +export const ID = 'hostsKpiAuthenticationsQuery'; export interface HostsKpiAuthenticationsArgs extends Omit { @@ -165,5 +165,13 @@ export const useHostsKpiAuthentications = ({ }; }, [hostsKpiAuthenticationsRequest, hostsKpiAuthenticationsSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, hostsKpiAuthenticationsResponse]; }; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.test.tsx new file mode 100644 index 0000000000000..f12b92f0661bc --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.test.tsx @@ -0,0 +1,28 @@ +/* + * 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 { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../../common/mock'; +import { useHostsKpiHosts } from './index'; + +describe('kpi hosts - hosts', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + skip: false, + }; + const { rerender } = renderHook(() => useHostsKpiHosts(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.tsx index fdce4dfe79591..63f0476c2b631 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.tsx @@ -26,7 +26,7 @@ import * as i18n from './translations'; import { getInspectResponse } from '../../../../helpers'; import { InspectResponse } from '../../../../types'; -const ID = 'hostsKpiHostsQuery'; +export const ID = 'hostsKpiHostsQuery'; export interface HostsKpiHostsArgs extends Omit { id: string; @@ -155,5 +155,13 @@ export const useHostsKpiHosts = ({ }; }, [hostsKpiHostsRequest, hostsKpiHostsSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, hostsKpiHostsResponse]; }; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/index.tsx deleted file mode 100644 index 8473d3971c66f..0000000000000 --- a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/index.tsx +++ /dev/null @@ -1,10 +0,0 @@ -/* - * 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. - */ - -export * from './authentications'; -export * from './hosts'; -export * from './unique_ips'; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.test.tsx new file mode 100644 index 0000000000000..ec8c73ad1d6a6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.test.tsx @@ -0,0 +1,28 @@ +/* + * 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 { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../../common/mock'; +import { useHostsKpiUniqueIps } from './index'; + +describe('kpi hosts - Unique Ips', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + skip: false, + }; + const { rerender } = renderHook(() => useHostsKpiUniqueIps(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.tsx index 5b9eeb2710ff3..25a9f76daf40f 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.tsx @@ -26,7 +26,7 @@ import * as i18n from './translations'; import { getInspectResponse } from '../../../../helpers'; import { InspectResponse } from '../../../../types'; -const ID = 'hostsKpiUniqueIpsQuery'; +export const ID = 'hostsKpiUniqueIpsQuery'; export interface HostsKpiUniqueIpsArgs extends Omit { @@ -163,5 +163,13 @@ export const useHostsKpiUniqueIps = ({ }; }, [hostsKpiUniqueIpsRequest, hostsKpiUniqueIpsSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, hostsKpiUniqueIpsResponse]; }; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.test.tsx new file mode 100644 index 0000000000000..e334465fdbc1c --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.test.tsx @@ -0,0 +1,30 @@ +/* + * 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 { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../common/mock'; +import { useUncommonProcesses } from './index'; +import { HostsType } from '../../store/model'; + +describe('useUncommonProcesses', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + type: HostsType.page, + skip: false, + }; + const { rerender } = renderHook(() => useUncommonProcesses(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx index 9548027520bd1..d196c4ea01af1 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/uncommon_processes/index.tsx @@ -34,7 +34,7 @@ import { InspectResponse } from '../../../types'; import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { useAppToasts } from '../../../common/hooks/use_app_toasts'; -const ID = 'hostsUncommonProcessesQuery'; +export const ID = 'hostsUncommonProcessesQuery'; export interface UncommonProcessesArgs { id: string; @@ -202,5 +202,13 @@ export const useUncommonProcesses = ({ }; }, [uncommonProcessesRequest, uncommonProcessesSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, uncommonProcessesResponse]; }; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.test.tsx new file mode 100644 index 0000000000000..9d31b477a851a --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.test.tsx @@ -0,0 +1,68 @@ +/* + * 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 { TestProviders } from '../../../common/mock'; +import { useAuthentications } from '../../containers/authentications'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; +import { AuthenticationsQueryTabBody } from './authentications_query_tab_body'; +import { HostsType } from '../../store/model'; + +jest.mock('../../containers/authentications'); +jest.mock('../../../common/containers/query_toggle'); +jest.mock('../../../common/lib/kibana'); + +describe('Authentications query tab body', () => { + const mockUseAuthentications = useAuthentications as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + indexNames: [], + setQuery: jest.fn(), + skip: false, + startDate: '2019-06-25T04:31:59.345Z', + endDate: '2019-06-25T06:31:59.345Z', + type: HostsType.page, + }; + beforeEach(() => { + jest.clearAllMocks(); + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseAuthentications.mockReturnValue([ + false, + { + authentications: [], + id: '123', + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + totalCount: 0, + pageInfo: { activePage: 1, fakeTotalCount: 100, showMorePagesIndicator: false }, + loadPage: jest.fn(), + refetch: jest.fn(), + }, + ]); + }); + it('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(mockUseAuthentications.mock.calls[0][0].skip).toEqual(false); + }); + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + render( + + + + ); + expect(mockUseAuthentications.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx index 879f0fce02fd5..1096085b93016 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx @@ -6,7 +6,7 @@ */ import { getOr } from 'lodash/fp'; -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { AuthenticationTable } from '../../components/authentications_table'; import { manageQuery } from '../../../common/components/page/manage_query'; import { useAuthentications } from '../../containers/authentications'; @@ -22,6 +22,7 @@ import * as i18n from '../translations'; import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution'; import { authenticationLensAttributes } from '../../../common/components/visualization_actions/lens_attributes/hosts/authentication'; import { LensAttributes } from '../../../common/components/visualization_actions/types'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const AuthenticationTableManage = manageQuery(AuthenticationTable); @@ -76,6 +77,11 @@ const AuthenticationsQueryTabBodyComponent: React.FC startDate, type, }) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [ loading, { authentications, totalCount, pageInfo, loadPage, id, inspect, isInspected, refetch }, @@ -84,7 +90,7 @@ const AuthenticationsQueryTabBodyComponent: React.FC endDate, filterQuery, indexNames, - skip, + skip: querySkip, startDate, type, }); @@ -119,6 +125,7 @@ const AuthenticationsQueryTabBodyComponent: React.FC loading={loading} loadPage={loadPage} refetch={refetch} + setQuerySkip={setQuerySkip} showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)} setQuery={setQuery} totalCount={totalCount} diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_score_tab_body.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_score_tab_body.test.tsx new file mode 100644 index 0000000000000..8b3a05cc3d88c --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_score_tab_body.test.tsx @@ -0,0 +1,81 @@ +/* + * 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 { TestProviders } from '../../../common/mock'; +import { useHostRiskScore, useHostRiskScoreKpi } from '../../../risk_score/containers'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; +import { HostRiskScoreQueryTabBody } from './host_risk_score_tab_body'; +import { HostsType } from '../../store/model'; + +jest.mock('../../../risk_score/containers'); +jest.mock('../../../common/containers/query_toggle'); +jest.mock('../../../common/lib/kibana'); + +describe('Host risk score query tab body', () => { + const mockUseHostRiskScore = useHostRiskScore as jest.Mock; + const mockUseHostRiskScoreKpi = useHostRiskScoreKpi as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + indexNames: [], + setQuery: jest.fn(), + skip: false, + startDate: '2019-06-25T04:31:59.345Z', + endDate: '2019-06-25T06:31:59.345Z', + type: HostsType.page, + }; + beforeEach(() => { + jest.clearAllMocks(); + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseHostRiskScoreKpi.mockReturnValue({ + loading: false, + severityCount: { + unknown: 12, + low: 12, + moderate: 12, + high: 12, + critical: 12, + }, + }); + mockUseHostRiskScore.mockReturnValue([ + false, + { + hosts: [], + id: '123', + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + totalCount: 0, + pageInfo: { activePage: 1, fakeTotalCount: 100, showMorePagesIndicator: false }, + loadPage: jest.fn(), + refetch: jest.fn(), + }, + ]); + }); + it('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(mockUseHostRiskScore.mock.calls[0][0].skip).toEqual(false); + expect(mockUseHostRiskScoreKpi.mock.calls[0][0].skip).toEqual(false); + }); + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + render( + + + + ); + expect(mockUseHostRiskScore.mock.calls[0][0].skip).toEqual(true); + expect(mockUseHostRiskScoreKpi.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_score_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_score_tab_body.tsx index 11a422fa0cd3d..11ba8d154cd81 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_score_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_score_tab_body.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { noop } from 'lodash/fp'; import { HostsComponentsQueryProps } from './types'; import { manageQuery } from '../../../common/components/page/manage_query'; @@ -18,6 +18,7 @@ import { useHostRiskScore, useHostRiskScoreKpi, } from '../../../risk_score/containers'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const HostRiskScoreTableManage = manageQuery(HostRiskScoreTable); @@ -43,15 +44,22 @@ export const HostRiskScoreQueryTabBody = ({ [activePage, limit] ); + const { toggleStatus } = useQueryToggle(HostRiskScoreQueryId.HOSTS_BY_RISK); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(!toggleStatus); + }, [toggleStatus]); + const [loading, { data, totalCount, inspect, isInspected, refetch }] = useHostRiskScore({ filterQuery, - skip, + skip: querySkip, pagination, sort, }); const { severityCount, loading: isKpiLoading } = useHostRiskScoreKpi({ filterQuery, + skip: querySkip, }); return ( @@ -65,6 +73,7 @@ export const HostRiskScoreQueryTabBody = ({ loadPage={noop} // It isn't necessary because PaginatedTable updates redux store and we load the page when activePage updates on the store refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} severityCount={severityCount} totalCount={totalCount} type={type} diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.test.tsx new file mode 100644 index 0000000000000..487934f30e8d6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.test.tsx @@ -0,0 +1,68 @@ +/* + * 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 { TestProviders } from '../../../common/mock'; +import { useAllHost } from '../../containers/hosts'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; +import { HostsQueryTabBody } from './hosts_query_tab_body'; +import { HostsType } from '../../store/model'; + +jest.mock('../../containers/hosts'); +jest.mock('../../../common/containers/query_toggle'); +jest.mock('../../../common/lib/kibana'); + +describe('Hosts query tab body', () => { + const mockUseAllHost = useAllHost as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + indexNames: [], + setQuery: jest.fn(), + skip: false, + startDate: '2019-06-25T04:31:59.345Z', + endDate: '2019-06-25T06:31:59.345Z', + type: HostsType.page, + }; + beforeEach(() => { + jest.clearAllMocks(); + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseAllHost.mockReturnValue([ + false, + { + hosts: [], + id: '123', + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + totalCount: 0, + pageInfo: { activePage: 1, fakeTotalCount: 100, showMorePagesIndicator: false }, + loadPage: jest.fn(), + refetch: jest.fn(), + }, + ]); + }); + it('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(mockUseAllHost.mock.calls[0][0].skip).toEqual(false); + }); + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + render( + + + + ); + expect(mockUseAllHost.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.tsx index cc43cfed4619d..b72e6572849d1 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/hosts_query_tab_body.tsx @@ -6,11 +6,12 @@ */ import { getOr } from 'lodash/fp'; -import React from 'react'; -import { useAllHost } from '../../containers/hosts'; +import React, { useEffect, useState } from 'react'; +import { useAllHost, ID } from '../../containers/hosts'; import { HostsComponentsQueryProps } from './types'; import { HostsTable } from '../../components/hosts_table'; import { manageQuery } from '../../../common/components/page/manage_query'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const HostsTableManage = manageQuery(HostsTable); @@ -25,8 +26,21 @@ export const HostsQueryTabBody = ({ startDate, type, }: HostsComponentsQueryProps) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [loading, { hosts, totalCount, pageInfo, loadPage, id, inspect, isInspected, refetch }] = - useAllHost({ docValueFields, endDate, filterQuery, indexNames, skip, startDate, type }); + useAllHost({ + docValueFields, + endDate, + filterQuery, + indexNames, + skip: querySkip, + startDate, + type, + }); return ( { + const mockUseUncommonProcesses = useUncommonProcesses as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + indexNames: [], + setQuery: jest.fn(), + skip: false, + startDate: '2019-06-25T04:31:59.345Z', + endDate: '2019-06-25T06:31:59.345Z', + type: HostsType.page, + }; + beforeEach(() => { + jest.clearAllMocks(); + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseUncommonProcesses.mockReturnValue([ + false, + { + uncommonProcesses: [], + id: '123', + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + totalCount: 0, + pageInfo: { activePage: 1, fakeTotalCount: 100, showMorePagesIndicator: false }, + loadPage: jest.fn(), + refetch: jest.fn(), + }, + ]); + }); + it('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(mockUseUncommonProcesses.mock.calls[0][0].skip).toEqual(false); + }); + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + render( + + + + ); + expect(mockUseUncommonProcesses.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/uncommon_process_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/uncommon_process_query_tab_body.tsx index 236b732a5af05..f6957fedd83c5 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/uncommon_process_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/uncommon_process_query_tab_body.tsx @@ -6,11 +6,12 @@ */ import { getOr } from 'lodash/fp'; -import React from 'react'; -import { useUncommonProcesses } from '../../containers/uncommon_processes'; +import React, { useEffect, useState } from 'react'; +import { useUncommonProcesses, ID } from '../../containers/uncommon_processes'; import { HostsComponentsQueryProps } from './types'; import { UncommonProcessTable } from '../../components/uncommon_process_table'; import { manageQuery } from '../../../common/components/page/manage_query'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const UncommonProcessTableManage = manageQuery(UncommonProcessTable); @@ -25,6 +26,11 @@ export const UncommonProcessQueryTabBody = ({ startDate, type, }: HostsComponentsQueryProps) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [ loading, { uncommonProcesses, totalCount, pageInfo, loadPage, id, inspect, isInspected, refetch }, @@ -33,7 +39,7 @@ export const UncommonProcessQueryTabBody = ({ endDate, filterQuery, indexNames, - skip, + skip: querySkip, startDate, type, }); @@ -49,6 +55,7 @@ export const UncommonProcessQueryTabBody = ({ loadPage={loadPage} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)} totalCount={totalCount} type={type} diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/__snapshots__/embeddable.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/embeddables/__snapshots__/embeddable.test.tsx.snap index 8835a3ac390f3..966512170c156 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/__snapshots__/embeddable.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/__snapshots__/embeddable.test.tsx.snap @@ -3,6 +3,7 @@ exports[`Embeddable it renders 1`] = `
(({ children }) => ( -
+
{children} diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.test.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.test.tsx index 4b8a5b6dd9940..2166d6b495e75 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.test.tsx @@ -109,7 +109,7 @@ describe('EmbeddedMapComponent', () => { beforeEach(() => { setQuery.mockClear(); - mockGetStorage.mockReturnValue(false); + mockGetStorage.mockReturnValue(true); }); afterEach(() => { @@ -190,36 +190,40 @@ describe('EmbeddedMapComponent', () => { }); test('map hidden on close', async () => { + mockGetStorage.mockReturnValue(false); const wrapper = mount( ); + expect(wrapper.find('[data-test-subj="siemEmbeddable"]').first().exists()).toEqual(false); + const container = wrapper.find('[data-test-subj="false-toggle-network-map"]').at(0); container.simulate('click'); await waitFor(() => { wrapper.update(); expect(mockSetStorage).toHaveBeenNthCalledWith(1, 'network_map_visbile', true); + expect(wrapper.find('[data-test-subj="siemEmbeddable"]').first().exists()).toEqual(true); }); }); test('map visible on open', async () => { - mockGetStorage.mockReturnValue(true); - const wrapper = mount( ); + expect(wrapper.find('[data-test-subj="siemEmbeddable"]').first().exists()).toEqual(true); const container = wrapper.find('[data-test-subj="true-toggle-network-map"]').at(0); container.simulate('click'); await waitFor(() => { wrapper.update(); expect(mockSetStorage).toHaveBeenNthCalledWith(1, 'network_map_visbile', false); + expect(wrapper.find('[data-test-subj="siemEmbeddable"]').first().exists()).toEqual(false); }); }); }); diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx index 803688bf21343..083f858dc7742 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/embedded_map.tsx @@ -245,6 +245,29 @@ export const EmbeddedMapComponent = ({ [storage] ); + const content = useMemo(() => { + if (!storageValue) { + return null; + } + return ( + + + + + + + {isIndexError ? ( + + ) : embeddable != null ? ( + + ) : ( + + )} + + + ); + }, [embeddable, isIndexError, portalNode, services, storageValue]); + return isError ? null : ( - - - - - - - {isIndexError ? ( - - ) : embeddable != null ? ( - - ) : ( - - )} - - + {content} ); }; diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/dns/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/dns/index.test.tsx new file mode 100644 index 0000000000000..d5dee1b84f8d7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/dns/index.test.tsx @@ -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 { useNetworkKpiDns } from '../../../containers/kpi_network/dns'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; +import { render } from '@testing-library/react'; +import { TestProviders } from '../../../../common/mock'; +import React from 'react'; +import { NetworkKpiDns } from './index'; + +jest.mock('../../../../common/containers/query_toggle'); +jest.mock('../../../containers/kpi_network/dns'); +jest.mock('../../../../hosts/components/kpi_hosts/common', () => ({ + KpiBaseComponentManage: () => , +})); + +describe('DNS KPI', () => { + const mockUseNetworkKpiDns = useNetworkKpiDns as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + from: '2019-06-25T04:31:59.345Z', + to: '2019-06-25T06:31:59.345Z', + indexNames: [], + narrowDateRange: jest.fn(), + setQuery: jest.fn(), + skip: false, + }; + beforeEach(() => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseNetworkKpiDns.mockReturnValue([ + false, + { + id: '123', + inspect: { + dsl: [], + response: [], + }, + refetch: jest.fn(), + }, + ]); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(mockUseNetworkKpiDns.mock.calls[0][0].skip).toEqual(false); + }); + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + render( + + + + ); + expect(mockUseNetworkKpiDns.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/dns/index.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/dns/index.tsx index 6291e7fd4dc12..94e81c2d80d4a 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/dns/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/dns/index.tsx @@ -5,15 +5,16 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { StatItems } from '../../../../common/components/stat_items'; import { kpiDnsQueriesLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/network/kpi_dns_queries'; +import { useNetworkKpiDns, ID } from '../../../containers/kpi_network/dns'; import { KpiBaseComponentManage } from '../../../../hosts/components/kpi_hosts/common'; -import { useNetworkKpiDns } from '../../../containers/kpi_network/dns'; import { NetworkKpiProps } from '../types'; import * as i18n from './translations'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; export const fieldsMapping: Readonly = [ { @@ -38,12 +39,17 @@ const NetworkKpiDnsComponent: React.FC = ({ setQuery, skip, }) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [loading, { refetch, id, inspect, ...data }] = useNetworkKpiDns({ filterQuery, endDate: to, indexNames, startDate: from, - skip, + skip: querySkip, }); return ( @@ -58,6 +64,7 @@ const NetworkKpiDnsComponent: React.FC = ({ narrowDateRange={narrowDateRange} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} /> ); }; diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/mock.ts b/x-pack/plugins/security_solution/public/network/components/kpi_network/mock.ts index 6f35c4dead250..f5ed1ebde6992 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/mock.ts +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/mock.ts @@ -227,7 +227,9 @@ export const mockEnableChartsData = { ], from: '2019-06-15T06:00:00.000Z', id: 'statItem', + loading: false, statKey: 'UniqueIps', + setQuerySkip: jest.fn(), to: '2019-06-18T06:00:00.000Z', narrowDateRange: mockNarrowDateRange, areaChartLensAttributes: kpiUniquePrivateIpsAreaLensAttributes, diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/network_events/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/network_events/index.test.tsx new file mode 100644 index 0000000000000..87f1a173740f3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/network_events/index.test.tsx @@ -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 { useNetworkKpiNetworkEvents } from '../../../containers/kpi_network/network_events'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; +import { render } from '@testing-library/react'; +import { TestProviders } from '../../../../common/mock'; +import React from 'react'; +import { NetworkKpiNetworkEvents } from './index'; + +jest.mock('../../../../common/containers/query_toggle'); +jest.mock('../../../containers/kpi_network/network_events'); +jest.mock('../../../../hosts/components/kpi_hosts/common', () => ({ + KpiBaseComponentManage: () => , +})); + +describe('Network Events KPI', () => { + const mockUseNetworkKpiNetworkEvents = useNetworkKpiNetworkEvents as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + from: '2019-06-25T04:31:59.345Z', + to: '2019-06-25T06:31:59.345Z', + indexNames: [], + narrowDateRange: jest.fn(), + setQuery: jest.fn(), + skip: false, + }; + beforeEach(() => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseNetworkKpiNetworkEvents.mockReturnValue([ + false, + { + id: '123', + inspect: { + dsl: [], + response: [], + }, + refetch: jest.fn(), + }, + ]); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(mockUseNetworkKpiNetworkEvents.mock.calls[0][0].skip).toEqual(false); + }); + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + render( + + + + ); + expect(mockUseNetworkKpiNetworkEvents.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/network_events/index.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/network_events/index.tsx index ad2487b65f1de..52aa98a117afa 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/network_events/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/network_events/index.tsx @@ -5,16 +5,16 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { euiPaletteColorBlind } from '@elastic/eui'; import { StatItems } from '../../../../common/components/stat_items'; -import { useNetworkKpiNetworkEvents } from '../../../containers/kpi_network/network_events'; - +import { ID, useNetworkKpiNetworkEvents } from '../../../containers/kpi_network/network_events'; import { NetworkKpiProps } from '../types'; import * as i18n from './translations'; import { kpiNetworkEventsLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/network/kpi_network_events'; import { KpiBaseComponentManage } from '../../../../hosts/components/kpi_hosts/common'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; const euiVisColorPalette = euiPaletteColorBlind(); const euiColorVis1 = euiVisColorPalette[1]; @@ -43,12 +43,17 @@ const NetworkKpiNetworkEventsComponent: React.FC = ({ setQuery, skip, }) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [loading, { refetch, id, inspect, ...data }] = useNetworkKpiNetworkEvents({ filterQuery, endDate: to, indexNames, startDate: from, - skip, + skip: querySkip, }); return ( @@ -63,6 +68,7 @@ const NetworkKpiNetworkEventsComponent: React.FC = ({ narrowDateRange={narrowDateRange} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} /> ); }; diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/tls_handshakes/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/tls_handshakes/index.test.tsx new file mode 100644 index 0000000000000..28bf73eb6b2d6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/tls_handshakes/index.test.tsx @@ -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 { useNetworkKpiTlsHandshakes } from '../../../containers/kpi_network/tls_handshakes'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; +import { render } from '@testing-library/react'; +import { TestProviders } from '../../../../common/mock'; +import React from 'react'; +import { NetworkKpiTlsHandshakes } from './index'; + +jest.mock('../../../../common/containers/query_toggle'); +jest.mock('../../../containers/kpi_network/tls_handshakes'); +jest.mock('../../../../hosts/components/kpi_hosts/common', () => ({ + KpiBaseComponentManage: () => , +})); + +describe('TLS Handshakes KPI', () => { + const mockUseNetworkKpiTlsHandshakes = useNetworkKpiTlsHandshakes as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + from: '2019-06-25T04:31:59.345Z', + to: '2019-06-25T06:31:59.345Z', + indexNames: [], + narrowDateRange: jest.fn(), + setQuery: jest.fn(), + skip: false, + }; + beforeEach(() => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseNetworkKpiTlsHandshakes.mockReturnValue([ + false, + { + id: '123', + inspect: { + dsl: [], + response: [], + }, + refetch: jest.fn(), + }, + ]); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(mockUseNetworkKpiTlsHandshakes.mock.calls[0][0].skip).toEqual(false); + }); + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + render( + + + + ); + expect(mockUseNetworkKpiTlsHandshakes.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/tls_handshakes/index.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/tls_handshakes/index.tsx index 0bdbd0a23d9f1..c25a4cd140108 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/tls_handshakes/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/tls_handshakes/index.tsx @@ -5,14 +5,15 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { StatItems } from '../../../../common/components/stat_items'; import { kpiTlsHandshakesLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/network/kpi_tls_handshakes'; +import { useNetworkKpiTlsHandshakes, ID } from '../../../containers/kpi_network/tls_handshakes'; import { KpiBaseComponentManage } from '../../../../hosts/components/kpi_hosts/common'; -import { useNetworkKpiTlsHandshakes } from '../../../containers/kpi_network/tls_handshakes'; import { NetworkKpiProps } from '../types'; import * as i18n from './translations'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; export const fieldsMapping: Readonly = [ { @@ -37,12 +38,17 @@ const NetworkKpiTlsHandshakesComponent: React.FC = ({ setQuery, skip, }) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [loading, { refetch, id, inspect, ...data }] = useNetworkKpiTlsHandshakes({ filterQuery, endDate: to, indexNames, startDate: from, - skip, + skip: querySkip, }); return ( @@ -57,6 +63,7 @@ const NetworkKpiTlsHandshakesComponent: React.FC = ({ narrowDateRange={narrowDateRange} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} /> ); }; diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_flows/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_flows/index.test.tsx new file mode 100644 index 0000000000000..c1a28bdc28692 --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_flows/index.test.tsx @@ -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 { useNetworkKpiUniqueFlows } from '../../../containers/kpi_network/unique_flows'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; +import { render } from '@testing-library/react'; +import { TestProviders } from '../../../../common/mock'; +import React from 'react'; +import { NetworkKpiUniqueFlows } from './index'; + +jest.mock('../../../../common/containers/query_toggle'); +jest.mock('../../../containers/kpi_network/unique_flows'); +jest.mock('../../../../hosts/components/kpi_hosts/common', () => ({ + KpiBaseComponentManage: () => , +})); + +describe('Unique Flows KPI', () => { + const mockUseNetworkKpiUniqueFlows = useNetworkKpiUniqueFlows as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + from: '2019-06-25T04:31:59.345Z', + to: '2019-06-25T06:31:59.345Z', + indexNames: [], + narrowDateRange: jest.fn(), + setQuery: jest.fn(), + skip: false, + }; + beforeEach(() => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseNetworkKpiUniqueFlows.mockReturnValue([ + false, + { + id: '123', + inspect: { + dsl: [], + response: [], + }, + refetch: jest.fn(), + }, + ]); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(mockUseNetworkKpiUniqueFlows.mock.calls[0][0].skip).toEqual(false); + }); + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + render( + + + + ); + expect(mockUseNetworkKpiUniqueFlows.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_flows/index.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_flows/index.tsx index 5c3624130b36f..d6874818ab901 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_flows/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_flows/index.tsx @@ -5,14 +5,15 @@ * 2.0. */ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { StatItems } from '../../../../common/components/stat_items'; import { kpiUniqueFlowIdsLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/network/kpi_unique_flow_ids'; +import { useNetworkKpiUniqueFlows, ID } from '../../../containers/kpi_network/unique_flows'; import { KpiBaseComponentManage } from '../../../../hosts/components/kpi_hosts/common'; -import { useNetworkKpiUniqueFlows } from '../../../containers/kpi_network/unique_flows'; import { NetworkKpiProps } from '../types'; import * as i18n from './translations'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; export const fieldsMapping: Readonly = [ { @@ -37,12 +38,17 @@ const NetworkKpiUniqueFlowsComponent: React.FC = ({ setQuery, skip, }) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [loading, { refetch, id, inspect, ...data }] = useNetworkKpiUniqueFlows({ filterQuery, endDate: to, indexNames, startDate: from, - skip, + skip: querySkip, }); return ( @@ -57,6 +63,7 @@ const NetworkKpiUniqueFlowsComponent: React.FC = ({ narrowDateRange={narrowDateRange} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} /> ); }; diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_private_ips/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_private_ips/index.test.tsx new file mode 100644 index 0000000000000..25807f3dc2cad --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_private_ips/index.test.tsx @@ -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 { useNetworkKpiUniquePrivateIps } from '../../../containers/kpi_network/unique_private_ips'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; +import { render } from '@testing-library/react'; +import { TestProviders } from '../../../../common/mock'; +import React from 'react'; +import { NetworkKpiUniquePrivateIps } from './index'; + +jest.mock('../../../../common/containers/query_toggle'); +jest.mock('../../../containers/kpi_network/unique_private_ips'); +jest.mock('../../../../hosts/components/kpi_hosts/common', () => ({ + KpiBaseComponentManage: () => , +})); + +describe('Unique Private IPs KPI', () => { + const mockUseNetworkKpiUniquePrivateIps = useNetworkKpiUniquePrivateIps as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + from: '2019-06-25T04:31:59.345Z', + to: '2019-06-25T06:31:59.345Z', + indexNames: [], + narrowDateRange: jest.fn(), + setQuery: jest.fn(), + skip: false, + }; + beforeEach(() => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseNetworkKpiUniquePrivateIps.mockReturnValue([ + false, + { + id: '123', + inspect: { + dsl: [], + response: [], + }, + refetch: jest.fn(), + }, + ]); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(mockUseNetworkKpiUniquePrivateIps.mock.calls[0][0].skip).toEqual(false); + }); + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + render( + + + + ); + expect(mockUseNetworkKpiUniquePrivateIps.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_private_ips/index.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_private_ips/index.tsx index e546deb7019e8..91791d09f8113 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_private_ips/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/unique_private_ips/index.tsx @@ -5,11 +5,14 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { euiPaletteColorBlind } from '@elastic/eui'; import { StatItems } from '../../../../common/components/stat_items'; -import { useNetworkKpiUniquePrivateIps } from '../../../containers/kpi_network/unique_private_ips'; +import { + useNetworkKpiUniquePrivateIps, + ID, +} from '../../../containers/kpi_network/unique_private_ips'; import { NetworkKpiProps } from '../types'; import * as i18n from './translations'; import { kpiUniquePrivateIpsSourceMetricLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_source_metric'; @@ -17,6 +20,7 @@ import { kpiUniquePrivateIpsDestinationMetricLensAttributes } from '../../../../ import { kpiUniquePrivateIpsAreaLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_area'; import { kpiUniquePrivateIpsBarLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/network/kpi_unique_private_ips_bar'; import { KpiBaseComponentManage } from '../../../../hosts/components/kpi_hosts/common'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; const euiVisColorPalette = euiPaletteColorBlind(); const euiColorVis2 = euiVisColorPalette[2]; @@ -62,12 +66,17 @@ const NetworkKpiUniquePrivateIpsComponent: React.FC = ({ setQuery, skip, }) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [loading, { refetch, id, inspect, ...data }] = useNetworkKpiUniquePrivateIps({ filterQuery, endDate: to, indexNames, startDate: from, - skip, + skip: querySkip, }); return ( @@ -82,6 +91,7 @@ const NetworkKpiUniquePrivateIpsComponent: React.FC = ({ narrowDateRange={narrowDateRange} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} /> ); }; diff --git a/x-pack/plugins/security_solution/public/network/components/network_dns_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/network_dns_table/__snapshots__/index.test.tsx.snap index 0119859d37672..c43df33721bf1 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_dns_table/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/components/network_dns_table/__snapshots__/index.test.tsx.snap @@ -141,6 +141,7 @@ exports[`NetworkTopNFlow Table Component rendering it renders the default Networ isInspect={false} loadPage={[MockFunction]} loading={false} + setQuerySkip={[MockFunction]} showMorePagesIndicator={true} totalCount={80} type="page" diff --git a/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.test.tsx index fc28067866146..2757baef2c1f4 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.test.tsx @@ -34,6 +34,19 @@ describe('NetworkTopNFlow Table Component', () => { let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); const mount = useMountAppended(); + const defaultProps = { + data: mockData.edges, + fakeTotalCount: getOr(50, 'fakeTotalCount', mockData.pageInfo), + id: 'dns', + isInspect: false, + loading: false, + loadPage, + setQuerySkip: jest.fn(), + showMorePagesIndicator: getOr(false, 'showMorePagesIndicator', mockData.pageInfo), + totalCount: mockData.totalCount, + type: networkModel.NetworkType.page, + }; + beforeEach(() => { store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); }); @@ -42,17 +55,7 @@ describe('NetworkTopNFlow Table Component', () => { test('it renders the default NetworkTopNFlow table', () => { const wrapper = shallow( - + ); @@ -64,17 +67,7 @@ describe('NetworkTopNFlow Table Component', () => { test('when you click on the column header, you should show the sorting icon', () => { const wrapper = mount( - + ); diff --git a/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.tsx b/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.tsx index 016a40f7e2a17..a87908d27e63d 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_dns_table/index.tsx @@ -32,6 +32,7 @@ interface NetworkDnsTableProps { isInspect: boolean; loading: boolean; loadPage: (newActivePage: number) => void; + setQuerySkip: (skip: boolean) => void; showMorePagesIndicator: boolean; totalCount: number; type: networkModel.NetworkType; @@ -56,6 +57,7 @@ const NetworkDnsTableComponent: React.FC = ({ loading, loadPage, showMorePagesIndicator, + setQuerySkip, totalCount, type, }) => { @@ -153,6 +155,7 @@ const NetworkDnsTableComponent: React.FC = ({ loadPage={loadPage} onChange={onChange} pageOfItems={data} + setQuerySkip={setQuerySkip} showMorePagesIndicator={showMorePagesIndicator} sorting={sorting} totalCount={fakeTotalCount} diff --git a/x-pack/plugins/security_solution/public/network/components/network_http_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/network_http_table/__snapshots__/index.test.tsx.snap index c5df0f6603fbf..c26c85d311959 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_http_table/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/components/network_http_table/__snapshots__/index.test.tsx.snap @@ -95,6 +95,7 @@ exports[`NetworkHttp Table Component rendering it renders the default NetworkHtt isInspect={false} loadPage={[MockFunction]} loading={false} + setQuerySkip={[MockFunction]} showMorePagesIndicator={false} totalCount={4} type="page" diff --git a/x-pack/plugins/security_solution/public/network/components/network_http_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/network_http_table/index.test.tsx index 2a85b31791f5a..e8bac5e54765c 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_http_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_http_table/index.test.tsx @@ -31,6 +31,18 @@ jest.mock('../../../common/components/link_to'); describe('NetworkHttp Table Component', () => { const loadPage = jest.fn(); const state: State = mockGlobalState; + const defaultProps = { + data: mockData.edges, + fakeTotalCount: getOr(50, 'fakeTotalCount', mockData.pageInfo), + id: 'http', + isInspect: false, + loading: false, + loadPage, + setQuerySkip: jest.fn(), + showMorePagesIndicator: getOr(false, 'showMorePagesIndicator', mockData.pageInfo), + totalCount: mockData.totalCount, + type: networkModel.NetworkType.page, + }; const { storage } = createSecuritySolutionStorageMock(); let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); @@ -44,17 +56,7 @@ describe('NetworkHttp Table Component', () => { test('it renders the default NetworkHttp table', () => { const wrapper = shallow( - + ); @@ -66,17 +68,7 @@ describe('NetworkHttp Table Component', () => { test('when you click on the column header, you should show the sorting icon', () => { const wrapper = mount( - + ); diff --git a/x-pack/plugins/security_solution/public/network/components/network_http_table/index.tsx b/x-pack/plugins/security_solution/public/network/components/network_http_table/index.tsx index 2f0c4a105606c..5bdfd45951292 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_http_table/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_http_table/index.tsx @@ -23,6 +23,7 @@ interface NetworkHttpTableProps { isInspect: boolean; loading: boolean; loadPage: (newActivePage: number) => void; + setQuerySkip: (skip: boolean) => void; showMorePagesIndicator: boolean; totalCount: number; type: networkModel.NetworkType; @@ -46,6 +47,7 @@ const NetworkHttpTableComponent: React.FC = ({ isInspect, loading, loadPage, + setQuerySkip, showMorePagesIndicator, totalCount, type, @@ -123,6 +125,7 @@ const NetworkHttpTableComponent: React.FC = ({ loadPage={loadPage} onChange={onChange} pageOfItems={data} + setQuerySkip={setQuerySkip} showMorePagesIndicator={showMorePagesIndicator} sorting={sorting} totalCount={fakeTotalCount} diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/__snapshots__/index.test.tsx.snap index ecf7d2d0cd16f..cd13be9cef38b 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/__snapshots__/index.test.tsx.snap @@ -151,6 +151,7 @@ exports[`NetworkTopCountries Table Component rendering it renders the IP Details isInspect={false} loadPage={[MockFunction]} loading={false} + setQuerySkip={[MockFunction]} showMorePagesIndicator={true} totalCount={524} type="details" @@ -308,6 +309,7 @@ exports[`NetworkTopCountries Table Component rendering it renders the default Ne isInspect={false} loadPage={[MockFunction]} loading={false} + setQuerySkip={[MockFunction]} showMorePagesIndicator={true} totalCount={524} type="page" diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.test.tsx index a0727fad65f18..12dc41961bdf5 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.test.tsx @@ -33,6 +33,24 @@ describe('NetworkTopCountries Table Component', () => { const loadPage = jest.fn(); const state: State = mockGlobalState; const mount = useMountAppended(); + const defaultProps = { + data: mockData.NetworkTopCountries.edges, + fakeTotalCount: getOr(50, 'fakeTotalCount', mockData.NetworkTopCountries.pageInfo), + flowTargeted: FlowTargetSourceDest.source, + id: 'topCountriesSource', + indexPattern: mockIndexPattern, + isInspect: false, + loading: false, + loadPage, + setQuerySkip: jest.fn(), + showMorePagesIndicator: getOr( + false, + 'showMorePagesIndicator', + mockData.NetworkTopCountries.pageInfo + ), + totalCount: mockData.NetworkTopCountries.totalCount, + type: networkModel.NetworkType.page, + }; const { storage } = createSecuritySolutionStorageMock(); let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); @@ -45,23 +63,7 @@ describe('NetworkTopCountries Table Component', () => { test('it renders the default NetworkTopCountries table', () => { const wrapper = shallow( - + ); @@ -70,23 +72,7 @@ describe('NetworkTopCountries Table Component', () => { test('it renders the IP Details NetworkTopCountries table', () => { const wrapper = shallow( - + ); @@ -98,23 +84,7 @@ describe('NetworkTopCountries Table Component', () => { test('when you click on the column header, you should show the sorting icon', () => { const wrapper = mount( - + ); expect(store.getState().network.page.queries.topCountriesSource.sort).toEqual({ diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx index 80de694f89484..00c9c7d0aaf30 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx @@ -35,6 +35,7 @@ interface NetworkTopCountriesTableProps { isInspect: boolean; loading: boolean; loadPage: (newActivePage: number) => void; + setQuerySkip: (skip: boolean) => void; showMorePagesIndicator: boolean; totalCount: number; type: networkModel.NetworkType; @@ -62,6 +63,7 @@ const NetworkTopCountriesTableComponent: React.FC isInspect, loading, loadPage, + setQuerySkip, showMorePagesIndicator, totalCount, type, @@ -170,6 +172,7 @@ const NetworkTopCountriesTableComponent: React.FC loadPage={loadPage} onChange={onChange} pageOfItems={data} + setQuerySkip={setQuerySkip} showMorePagesIndicator={showMorePagesIndicator} sorting={{ field, direction: sort.direction }} totalCount={fakeTotalCount} diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/__snapshots__/index.test.tsx.snap index 07874f9f39f0b..7909eba5b0d88 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/__snapshots__/index.test.tsx.snap @@ -99,6 +99,7 @@ exports[`NetworkTopNFlow Table Component rendering it renders the default Networ isInspect={false} loadPage={[MockFunction]} loading={false} + setQuerySkip={[MockFunction]} showMorePagesIndicator={true} totalCount={524} type="details" @@ -204,6 +205,7 @@ exports[`NetworkTopNFlow Table Component rendering it renders the default Networ isInspect={false} loadPage={[MockFunction]} loading={false} + setQuerySkip={[MockFunction]} showMorePagesIndicator={true} totalCount={524} type="page" diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.test.tsx index e2b9447b58806..b5df028f4d7a4 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.test.tsx @@ -35,6 +35,19 @@ describe('NetworkTopNFlow Table Component', () => { const { storage } = createSecuritySolutionStorageMock(); let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); const mount = useMountAppended(); + const defaultProps = { + data: mockData.edges, + fakeTotalCount: getOr(50, 'fakeTotalCount', mockData.pageInfo), + flowTargeted: FlowTargetSourceDest.source, + id: 'topNFlowSource', + isInspect: false, + loading: false, + loadPage, + setQuerySkip: jest.fn(), + showMorePagesIndicator: getOr(false, 'showMorePagesIndicator', mockData.pageInfo), + totalCount: mockData.totalCount, + type: networkModel.NetworkType.page, + }; beforeEach(() => { store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); @@ -44,18 +57,7 @@ describe('NetworkTopNFlow Table Component', () => { test('it renders the default NetworkTopNFlow table on the Network page', () => { const wrapper = shallow( - + ); @@ -65,18 +67,7 @@ describe('NetworkTopNFlow Table Component', () => { test('it renders the default NetworkTopNFlow table on the IP Details page', () => { const wrapper = shallow( - + ); @@ -88,18 +79,7 @@ describe('NetworkTopNFlow Table Component', () => { test('when you click on the column header, you should show the sorting icon', () => { const wrapper = mount( - + ); expect(store.getState().network.page.queries.topNFlowSource.sort).toEqual({ diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.tsx b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.tsx index a612d3e4e1093..12895226a82eb 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.tsx @@ -31,6 +31,7 @@ interface NetworkTopNFlowTableProps { isInspect: boolean; loading: boolean; loadPage: (newActivePage: number) => void; + setQuerySkip: (skip: boolean) => void; showMorePagesIndicator: boolean; totalCount: number; type: networkModel.NetworkType; @@ -57,6 +58,7 @@ const NetworkTopNFlowTableComponent: React.FC = ({ isInspect, loading, loadPage, + setQuerySkip, showMorePagesIndicator, totalCount, type, @@ -166,6 +168,7 @@ const NetworkTopNFlowTableComponent: React.FC = ({ loadPage={loadPage} onChange={onChange} pageOfItems={data} + setQuerySkip={setQuerySkip} showMorePagesIndicator={showMorePagesIndicator} sorting={sorting} totalCount={fakeTotalCount} diff --git a/x-pack/plugins/security_solution/public/network/components/tls_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/tls_table/index.test.tsx index 3a1a5efef6b89..a54b219985817 100644 --- a/x-pack/plugins/security_solution/public/network/components/tls_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/tls_table/index.test.tsx @@ -29,7 +29,18 @@ jest.mock('../../../common/lib/kibana'); describe('Tls Table Component', () => { const loadPage = jest.fn(); const state: State = mockGlobalState; - + const defaultProps = { + data: mockTlsData.edges, + fakeTotalCount: getOr(50, 'fakeTotalCount', mockTlsData.pageInfo), + id: 'tls', + isInspect: false, + loading: false, + loadPage, + setQuerySkip: jest.fn(), + showMorePagesIndicator: getOr(false, 'showMorePagesIndicator', mockTlsData.pageInfo), + totalCount: 1, + type: networkModel.NetworkType.details, + }; const { storage } = createSecuritySolutionStorageMock(); let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); const mount = useMountAppended(); @@ -42,17 +53,7 @@ describe('Tls Table Component', () => { test('it renders the default Domains table', () => { const wrapper = shallow( - + ); @@ -64,17 +65,7 @@ describe('Tls Table Component', () => { test('when you click on the column header, you should show the sorting icon', () => { const wrapper = mount( - + ); expect(store.getState().network.details.queries?.tls.sort).toEqual({ diff --git a/x-pack/plugins/security_solution/public/network/components/tls_table/index.tsx b/x-pack/plugins/security_solution/public/network/components/tls_table/index.tsx index 34a218db39fac..60079e50f27ce 100644 --- a/x-pack/plugins/security_solution/public/network/components/tls_table/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/tls_table/index.tsx @@ -33,6 +33,7 @@ interface TlsTableProps { isInspect: boolean; loading: boolean; loadPage: (newActivePage: number) => void; + setQuerySkip: (skip: boolean) => void; showMorePagesIndicator: boolean; totalCount: number; type: networkModel.NetworkType; @@ -58,6 +59,7 @@ const TlsTableComponent: React.FC = ({ isInspect, loading, loadPage, + setQuerySkip, showMorePagesIndicator, totalCount, type, @@ -135,6 +137,7 @@ const TlsTableComponent: React.FC = ({ loadPage={loadPage} onChange={onChange} pageOfItems={data} + setQuerySkip={setQuerySkip} sorting={getSortField(sort)} totalCount={fakeTotalCount} updateActivePage={updateActivePage} diff --git a/x-pack/plugins/security_solution/public/network/components/users_table/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/users_table/index.test.tsx index 3861433b4dcb0..95e014332d42a 100644 --- a/x-pack/plugins/security_solution/public/network/components/users_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/users_table/index.test.tsx @@ -40,22 +40,25 @@ describe('Users Table Component', () => { store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); }); + const defaultProps = { + data: mockUsersData.edges, + flowTarget: FlowTarget.source, + fakeTotalCount: getOr(50, 'fakeTotalCount', mockUsersData.pageInfo), + id: 'user', + isInspect: false, + loading: false, + loadPage, + setQuerySkip: jest.fn(), + showMorePagesIndicator: getOr(false, 'showMorePagesIndicator', mockUsersData.pageInfo), + totalCount: 1, + type: networkModel.NetworkType.details, + }; + describe('Rendering', () => { test('it renders the default Users table', () => { const wrapper = shallow( - + ); @@ -67,18 +70,7 @@ describe('Users Table Component', () => { test('when you click on the column header, you should show the sorting icon', () => { const wrapper = mount( - + ); expect(store.getState().network.details.queries?.users.sort).toEqual({ diff --git a/x-pack/plugins/security_solution/public/network/components/users_table/index.tsx b/x-pack/plugins/security_solution/public/network/components/users_table/index.tsx index 66c36208fd98a..efbe5b7d1d010 100644 --- a/x-pack/plugins/security_solution/public/network/components/users_table/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/users_table/index.tsx @@ -38,6 +38,7 @@ interface UsersTableProps { isInspect: boolean; loading: boolean; loadPage: (newActivePage: number) => void; + setQuerySkip: (skip: boolean) => void; showMorePagesIndicator: boolean; totalCount: number; type: networkModel.NetworkType; @@ -64,6 +65,7 @@ const UsersTableComponent: React.FC = ({ isInspect, loading, loadPage, + setQuerySkip, showMorePagesIndicator, totalCount, type, @@ -141,6 +143,7 @@ const UsersTableComponent: React.FC = ({ loadPage={loadPage} onChange={onChange} pageOfItems={data} + setQuerySkip={setQuerySkip} sorting={getSortField(sort)} totalCount={fakeTotalCount} updateActivePage={updateActivePage} diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.test.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.test.tsx new file mode 100644 index 0000000000000..44b8472a0606c --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.test.tsx @@ -0,0 +1,28 @@ +/* + * 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 { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../../common/mock'; +import { useNetworkKpiDns } from './index'; + +describe('kpi network - dns', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + skip: false, + }; + const { rerender } = renderHook(() => useNetworkKpiDns(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.tsx index 63fb751572b0b..89f58f547bd75 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.tsx @@ -30,7 +30,7 @@ import { import { getInspectResponse } from '../../../../helpers'; import { InspectResponse } from '../../../../types'; -const ID = 'networkKpiDnsQuery'; +export const ID = 'networkKpiDnsQuery'; export interface NetworkKpiDnsArgs { dnsQueries: number; @@ -160,5 +160,13 @@ export const useNetworkKpiDns = ({ }; }, [networkKpiDnsRequest, networkKpiDnsSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, networkKpiDnsResponse]; }; diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/index.tsx deleted file mode 100644 index 550cefcf13e92..0000000000000 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/index.tsx +++ /dev/null @@ -1,12 +0,0 @@ -/* - * 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. - */ - -export * from './dns'; -export * from './network_events'; -export * from './tls_handshakes'; -export * from './unique_flows'; -export * from './unique_private_ips'; diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.test.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.test.tsx new file mode 100644 index 0000000000000..4171a86fae9cc --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.test.tsx @@ -0,0 +1,28 @@ +/* + * 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 { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../../common/mock'; +import { useNetworkKpiNetworkEvents } from './index'; + +describe('kpi network - network events', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + skip: false, + }; + const { rerender } = renderHook(() => useNetworkKpiNetworkEvents(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx index 4ecf455a31724..51a5367446b6e 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx @@ -30,7 +30,7 @@ import { import { getInspectResponse } from '../../../../helpers'; import { InspectResponse } from '../../../../types'; -const ID = 'networkKpiNetworkEventsQuery'; +export const ID = 'networkKpiNetworkEventsQuery'; export interface NetworkKpiNetworkEventsArgs { networkEvents: number; @@ -163,5 +163,13 @@ export const useNetworkKpiNetworkEvents = ({ }; }, [networkKpiNetworkEventsRequest, networkKpiNetworkEventsSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, networkKpiNetworkEventsResponse]; }; diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.test.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.test.tsx new file mode 100644 index 0000000000000..bad0e6ad71512 --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.test.tsx @@ -0,0 +1,28 @@ +/* + * 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 { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../../common/mock'; +import { useNetworkKpiTlsHandshakes } from './index'; + +describe('kpi network - tls handshakes', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + skip: false, + }; + const { rerender } = renderHook(() => useNetworkKpiTlsHandshakes(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx index 2dbf909334b15..ba42d79ad0eed 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx @@ -30,7 +30,7 @@ import { import { getInspectResponse } from '../../../../helpers'; import { InspectResponse } from '../../../../types'; -const ID = 'networkKpiTlsHandshakesQuery'; +export const ID = 'networkKpiTlsHandshakesQuery'; export interface NetworkKpiTlsHandshakesArgs { tlsHandshakes: number; @@ -163,5 +163,13 @@ export const useNetworkKpiTlsHandshakes = ({ }; }, [networkKpiTlsHandshakesRequest, networkKpiTlsHandshakesSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, networkKpiTlsHandshakesResponse]; }; diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.test.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.test.tsx new file mode 100644 index 0000000000000..83cb2a40aabce --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.test.tsx @@ -0,0 +1,28 @@ +/* + * 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 { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../../common/mock'; +import { useNetworkKpiUniqueFlows } from './index'; + +describe('kpi network - unique flows', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + skip: false, + }; + const { rerender } = renderHook(() => useNetworkKpiUniqueFlows(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx index 612aac175fd9a..130efc8d755a6 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx @@ -29,7 +29,7 @@ import { import { getInspectResponse } from '../../../../helpers'; import { InspectResponse } from '../../../../types'; -const ID = 'networkKpiUniqueFlowsQuery'; +export const ID = 'networkKpiUniqueFlowsQuery'; export interface NetworkKpiUniqueFlowsArgs { uniqueFlowId: number; @@ -84,7 +84,6 @@ export const useNetworkKpiUniqueFlows = ({ const asyncSearch = async () => { abortCtrl.current = new AbortController(); setLoading(true); - searchSubscription$.current = data.search .search( request, @@ -155,5 +154,13 @@ export const useNetworkKpiUniqueFlows = ({ }; }, [networkKpiUniqueFlowsRequest, networkKpiUniqueFlowsSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, networkKpiUniqueFlowsResponse]; }; diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.test.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.test.tsx new file mode 100644 index 0000000000000..370c4e671e886 --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.test.tsx @@ -0,0 +1,28 @@ +/* + * 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 { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../../common/mock'; +import { useNetworkKpiUniquePrivateIps } from './index'; + +describe('kpi network - unique private ips', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + skip: false, + }; + const { rerender } = renderHook(() => useNetworkKpiUniquePrivateIps(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx index 42a8e30a8f906..b68c4fcb698c0 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx @@ -31,7 +31,7 @@ import { import { getInspectResponse } from '../../../../helpers'; import { InspectResponse } from '../../../../types'; -const ID = 'networkKpiUniquePrivateIpsQuery'; +export const ID = 'networkKpiUniquePrivateIpsQuery'; export interface NetworkKpiUniquePrivateIpsArgs { uniqueDestinationPrivateIps: number; @@ -175,5 +175,13 @@ export const useNetworkKpiUniquePrivateIps = ({ }; }, [networkKpiUniquePrivateIpsRequest, networkKpiUniquePrivateIpsSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, networkKpiUniquePrivateIpsResponse]; }; diff --git a/x-pack/plugins/security_solution/public/network/containers/network_dns/index.test.tsx b/x-pack/plugins/security_solution/public/network/containers/network_dns/index.test.tsx new file mode 100644 index 0000000000000..f303cdf85a5f8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/network_dns/index.test.tsx @@ -0,0 +1,31 @@ +/* + * 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 { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../common/mock'; +import { useNetworkDns } from './index'; +import { NetworkType } from '../../store/model'; + +describe('useNetworkDns', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + docValueFields: [], + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + type: NetworkType.page, + skip: false, + }; + const { rerender } = renderHook(() => useNetworkDns(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx index 47e60f27a7dbd..86949777dd535 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx @@ -32,7 +32,7 @@ import { getInspectResponse } from '../../../helpers'; import { InspectResponse } from '../../../types'; import { useAppToasts } from '../../../common/hooks/use_app_toasts'; -const ID = 'networkDnsQuery'; +export const ID = 'networkDnsQuery'; export interface NetworkDnsArgs { id: string; @@ -207,5 +207,13 @@ export const useNetworkDns = ({ }; }, [networkDnsRequest, networkDnsSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, networkDnsResponse]; }; diff --git a/x-pack/plugins/security_solution/public/network/containers/network_http/index.test.tsx b/x-pack/plugins/security_solution/public/network/containers/network_http/index.test.tsx new file mode 100644 index 0000000000000..b687896efcea4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/network_http/index.test.tsx @@ -0,0 +1,31 @@ +/* + * 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 { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../common/mock'; +import { useNetworkHttp } from './index'; +import { NetworkType } from '../../store/model'; + +describe('useNetworkHttp', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + docValueFields: [], + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + type: NetworkType.page, + skip: false, + }; + const { rerender } = renderHook(() => useNetworkHttp(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx index 98105f5cac25a..eba2b22f30e29 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx @@ -31,7 +31,7 @@ import { InspectResponse } from '../../../types'; import { getInspectResponse } from '../../../helpers'; import { useAppToasts } from '../../../common/hooks/use_app_toasts'; -const ID = 'networkHttpQuery'; +export const ID = 'networkHttpQuery'; export interface NetworkHttpArgs { id: string; @@ -94,7 +94,7 @@ export const useNetworkHttp = ({ const [networkHttpResponse, setNetworkHttpResponse] = useState({ networkHttp: [], - id: ID, + id, inspect: { dsl: [], response: [], @@ -116,11 +116,9 @@ export const useNetworkHttp = ({ if (request == null || skip) { return; } - const asyncSearch = async () => { abortCtrl.current = new AbortController(); setLoading(true); - searchSubscription$.current = data.search .search(request, { strategy: 'securitySolutionSearchStrategy', @@ -193,5 +191,13 @@ export const useNetworkHttp = ({ }; }, [networkHttpRequest, networkHttpSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, networkHttpResponse]; }; diff --git a/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.test.tsx b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.test.tsx new file mode 100644 index 0000000000000..fe7507c85567a --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.test.tsx @@ -0,0 +1,33 @@ +/* + * 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 { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../common/mock'; +import { useNetworkTopCountries } from './index'; +import { NetworkType } from '../../store/model'; +import { FlowTargetSourceDest } from '../../../../common/search_strategy'; + +describe('useNetworkTopCountries', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + docValueFields: [], + flowTarget: FlowTargetSourceDest.source, + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + type: NetworkType.page, + skip: false, + }; + const { rerender } = renderHook(() => useNetworkTopCountries(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx index f64ee85ab7cf0..6110e84804fe3 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx @@ -32,7 +32,7 @@ import * as i18n from './translations'; import { useTransforms } from '../../../transforms/containers/use_transforms'; import { useAppToasts } from '../../../common/hooks/use_app_toasts'; -const ID = 'networkTopCountriesQuery'; +export const ID = 'networkTopCountriesQuery'; export interface NetworkTopCountriesArgs { id: string; @@ -218,5 +218,13 @@ export const useNetworkTopCountries = ({ }; }, [networkTopCountriesRequest, networkTopCountriesSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, networkTopCountriesResponse]; }; diff --git a/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.test.tsx b/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.test.tsx new file mode 100644 index 0000000000000..c31dec3ce0aed --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.test.tsx @@ -0,0 +1,33 @@ +/* + * 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 { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../common/mock'; +import { useNetworkTopNFlow } from './index'; +import { NetworkType } from '../../store/model'; +import { FlowTargetSourceDest } from '../../../../common/search_strategy'; + +describe('useNetworkTopNFlow', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + docValueFields: [], + flowTarget: FlowTargetSourceDest.source, + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + type: NetworkType.page, + skip: false, + }; + const { rerender } = renderHook(() => useNetworkTopNFlow(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx index 0b4c164782f3d..022b76c315c17 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx @@ -32,7 +32,7 @@ import * as i18n from './translations'; import { useTransforms } from '../../../transforms/containers/use_transforms'; import { useAppToasts } from '../../../common/hooks/use_app_toasts'; -const ID = 'networkTopNFlowQuery'; +export const ID = 'networkTopNFlowQuery'; export interface NetworkTopNFlowArgs { id: string; @@ -215,5 +215,13 @@ export const useNetworkTopNFlow = ({ }; }, [networkTopNFlowRequest, networkTopNFlowSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, networkTopNFlowResponse]; }; diff --git a/x-pack/plugins/security_solution/public/network/containers/tls/index.test.tsx b/x-pack/plugins/security_solution/public/network/containers/tls/index.test.tsx new file mode 100644 index 0000000000000..6b236d4ddfb20 --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/tls/index.test.tsx @@ -0,0 +1,34 @@ +/* + * 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 { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../common/mock'; +import { useNetworkTls } from './index'; +import { NetworkType } from '../../store/model'; +import { FlowTargetSourceDest } from '../../../../common/search_strategy'; + +describe('useNetworkTls', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + docValueFields: [], + flowTarget: FlowTargetSourceDest.source, + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + ip: '1.1.1.1', + type: NetworkType.page, + skip: false, + }; + const { rerender } = renderHook(() => useNetworkTls(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx b/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx index 754f0cac8868c..ed771455446c0 100644 --- a/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx @@ -29,7 +29,7 @@ import { getInspectResponse } from '../../../helpers'; import { FlowTargetSourceDest, PageInfoPaginated } from '../../../../common/search_strategy'; import { useAppToasts } from '../../../common/hooks/use_app_toasts'; -const ID = 'networkTlsQuery'; +export const ID = 'networkTlsQuery'; export interface NetworkTlsArgs { id: string; @@ -196,5 +196,13 @@ export const useNetworkTls = ({ }; }, [networkTlsRequest, networkTlsSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, networkTlsResponse]; }; diff --git a/x-pack/plugins/security_solution/public/network/containers/users/index.test.tsx b/x-pack/plugins/security_solution/public/network/containers/users/index.test.tsx new file mode 100644 index 0000000000000..4a6c1fac4191c --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/users/index.test.tsx @@ -0,0 +1,34 @@ +/* + * 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 { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../common/mock'; +import { useNetworkUsers } from './index'; +import { NetworkType } from '../../store/model'; +import { FlowTarget } from '../../../../common/search_strategy'; + +describe('useNetworkUsers', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + docValueFields: [], + ip: '1.1.1.1', + flowTarget: FlowTarget.source, + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + type: NetworkType.page, + skip: false, + }; + const { rerender } = renderHook(() => useNetworkUsers(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/network/containers/users/index.tsx b/x-pack/plugins/security_solution/public/network/containers/users/index.tsx index d4be09f97591d..9ad2c59f6bb79 100644 --- a/x-pack/plugins/security_solution/public/network/containers/users/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/users/index.tsx @@ -31,7 +31,7 @@ import { InspectResponse } from '../../../types'; import { PageInfoPaginated } from '../../../../common/search_strategy'; import { useAppToasts } from '../../../common/hooks/use_app_toasts'; -const ID = 'networkUsersQuery'; +export const ID = 'networkUsersQuery'; export interface NetworkUsersArgs { id: string; @@ -195,5 +195,13 @@ export const useNetworkUsers = ({ }; }, [networkUsersRequest, networkUsersSearch]); + useEffect(() => { + if (skip) { + setLoading(false); + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, networkUsersResponse]; }; diff --git a/x-pack/plugins/security_solution/public/network/pages/details/network_http_query_table.tsx b/x-pack/plugins/security_solution/public/network/pages/details/network_http_query_table.tsx index 4a4004b9a5f0c..d615bd8264b4b 100644 --- a/x-pack/plugins/security_solution/public/network/pages/details/network_http_query_table.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/details/network_http_query_table.tsx @@ -5,12 +5,13 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { getOr } from 'lodash/fp'; import { manageQuery } from '../../../common/components/page/manage_query'; import { OwnProps } from './types'; -import { useNetworkHttp } from '../../containers/network_http'; +import { useNetworkHttp, ID } from '../../containers/network_http'; import { NetworkHttpTable } from '../../components/network_http_table'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const NetworkHttpTableManage = manageQuery(NetworkHttpTable); @@ -24,6 +25,11 @@ export const NetworkHttpQueryTable = ({ startDate, type, }: OwnProps) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [ loading, { id, inspect, isInspected, loadPage, networkHttp, pageInfo, refetch, totalCount }, @@ -32,7 +38,7 @@ export const NetworkHttpQueryTable = ({ filterQuery, indexNames, ip, - skip, + skip: querySkip, startDate, type, }); @@ -48,6 +54,7 @@ export const NetworkHttpQueryTable = ({ loadPage={loadPage} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)} totalCount={totalCount} type={type} diff --git a/x-pack/plugins/security_solution/public/network/pages/details/network_top_countries_query_table.tsx b/x-pack/plugins/security_solution/public/network/pages/details/network_top_countries_query_table.tsx index 742f0f6ff9a9d..4243635ebb218 100644 --- a/x-pack/plugins/security_solution/public/network/pages/details/network_top_countries_query_table.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/details/network_top_countries_query_table.tsx @@ -5,12 +5,13 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { getOr } from 'lodash/fp'; import { manageQuery } from '../../../common/components/page/manage_query'; import { NetworkWithIndexComponentsQueryTableProps } from './types'; -import { useNetworkTopCountries } from '../../containers/network_top_countries'; +import { useNetworkTopCountries, ID } from '../../containers/network_top_countries'; import { NetworkTopCountriesTable } from '../../components/network_top_countries_table'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const NetworkTopCountriesTableManage = manageQuery(NetworkTopCountriesTable); @@ -26,6 +27,11 @@ export const NetworkTopCountriesQueryTable = ({ type, indexPattern, }: NetworkWithIndexComponentsQueryTableProps) => { + const { toggleStatus } = useQueryToggle(`${ID}-${flowTarget}`); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [ loading, { id, inspect, isInspected, loadPage, networkTopCountries, pageInfo, refetch, totalCount }, @@ -35,7 +41,7 @@ export const NetworkTopCountriesQueryTable = ({ filterQuery, indexNames, ip, - skip, + skip: querySkip, startDate, type, }); @@ -53,6 +59,7 @@ export const NetworkTopCountriesQueryTable = ({ loadPage={loadPage} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)} totalCount={totalCount} type={type} diff --git a/x-pack/plugins/security_solution/public/network/pages/details/network_top_n_flow_query_table.tsx b/x-pack/plugins/security_solution/public/network/pages/details/network_top_n_flow_query_table.tsx index 374dd6e6564e3..3df5397600c12 100644 --- a/x-pack/plugins/security_solution/public/network/pages/details/network_top_n_flow_query_table.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/details/network_top_n_flow_query_table.tsx @@ -6,11 +6,12 @@ */ import { getOr } from 'lodash/fp'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { manageQuery } from '../../../common/components/page/manage_query'; import { NetworkTopNFlowTable } from '../../components/network_top_n_flow_table'; -import { useNetworkTopNFlow } from '../../containers/network_top_n_flow'; +import { useNetworkTopNFlow, ID } from '../../containers/network_top_n_flow'; import { NetworkWithIndexComponentsQueryTableProps } from './types'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const NetworkTopNFlowTableManage = manageQuery(NetworkTopNFlowTable); @@ -25,6 +26,11 @@ export const NetworkTopNFlowQueryTable = ({ startDate, type, }: NetworkWithIndexComponentsQueryTableProps) => { + const { toggleStatus } = useQueryToggle(`${ID}-${flowTarget}`); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [ loading, { id, inspect, isInspected, loadPage, networkTopNFlow, pageInfo, refetch, totalCount }, @@ -34,7 +40,7 @@ export const NetworkTopNFlowQueryTable = ({ flowTarget, indexNames, ip, - skip, + skip: querySkip, startDate, type, }); @@ -51,6 +57,7 @@ export const NetworkTopNFlowQueryTable = ({ loadPage={loadPage} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)} totalCount={totalCount} type={type} diff --git a/x-pack/plugins/security_solution/public/network/pages/details/tls_query_table.tsx b/x-pack/plugins/security_solution/public/network/pages/details/tls_query_table.tsx index d3da639c8cf98..f4539e1ffc63d 100644 --- a/x-pack/plugins/security_solution/public/network/pages/details/tls_query_table.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/details/tls_query_table.tsx @@ -6,11 +6,12 @@ */ import { getOr } from 'lodash/fp'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { manageQuery } from '../../../common/components/page/manage_query'; import { TlsTable } from '../../components/tls_table'; -import { useNetworkTls } from '../../containers/tls'; +import { ID, useNetworkTls } from '../../containers/tls'; import { TlsQueryTableComponentProps } from './types'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const TlsTableManage = manageQuery(TlsTable); @@ -25,6 +26,11 @@ export const TlsQueryTable = ({ startDate, type, }: TlsQueryTableComponentProps) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [loading, { id, inspect, isInspected, tls, totalCount, pageInfo, loadPage, refetch }] = useNetworkTls({ endDate, @@ -32,7 +38,7 @@ export const TlsQueryTable = ({ flowTarget, indexNames, ip, - skip, + skip: querySkip, startDate, type, }); @@ -49,6 +55,7 @@ export const TlsQueryTable = ({ showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} totalCount={totalCount} type={type} /> diff --git a/x-pack/plugins/security_solution/public/network/pages/details/users_query_table.tsx b/x-pack/plugins/security_solution/public/network/pages/details/users_query_table.tsx index a73835985d7c5..9eb27c399ffbf 100644 --- a/x-pack/plugins/security_solution/public/network/pages/details/users_query_table.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/details/users_query_table.tsx @@ -5,12 +5,13 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { getOr } from 'lodash/fp'; import { manageQuery } from '../../../common/components/page/manage_query'; -import { useNetworkUsers } from '../../containers/users'; +import { useNetworkUsers, ID } from '../../containers/users'; import { NetworkComponentsQueryProps } from './types'; import { UsersTable } from '../../components/users_table'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const UsersTableManage = manageQuery(UsersTable); @@ -24,6 +25,11 @@ export const UsersQueryTable = ({ startDate, type, }: NetworkComponentsQueryProps) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [ loading, { id, inspect, isInspected, networkUsers, totalCount, pageInfo, loadPage, refetch }, @@ -32,7 +38,7 @@ export const UsersQueryTable = ({ filterQuery, flowTarget, ip, - skip, + skip: querySkip, startDate, }); @@ -49,6 +55,7 @@ export const UsersQueryTable = ({ showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} totalCount={totalCount} type={type} /> diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/countries_query_tab_body.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/countries_query_tab_body.tsx index e4bb00d1cb632..b390ccdcfff82 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/countries_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/countries_query_tab_body.tsx @@ -5,15 +5,16 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { getOr } from 'lodash/fp'; import { NetworkTopCountriesTable } from '../../components/network_top_countries_table'; -import { useNetworkTopCountries } from '../../containers/network_top_countries'; +import { useNetworkTopCountries, ID } from '../../containers/network_top_countries'; import { networkModel } from '../../store'; import { manageQuery } from '../../../common/components/page/manage_query'; import { IPsQueryTabBodyProps as CountriesQueryTabBodyProps } from './types'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const NetworkTopCountriesTableManage = manageQuery(NetworkTopCountriesTable); @@ -27,6 +28,11 @@ export const CountriesQueryTabBody = ({ indexPattern, flowTarget, }: CountriesQueryTabBodyProps) => { + const { toggleStatus } = useQueryToggle(`${ID}-${flowTarget}`); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [ loading, { id, inspect, isInspected, loadPage, networkTopCountries, pageInfo, refetch, totalCount }, @@ -35,7 +41,7 @@ export const CountriesQueryTabBody = ({ flowTarget, filterQuery, indexNames, - skip, + skip: querySkip, startDate, type: networkModel.NetworkType.page, }); @@ -53,6 +59,7 @@ export const CountriesQueryTabBody = ({ loadPage={loadPage} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)} totalCount={totalCount} type={networkModel.NetworkType.page} diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx index 21404690438a0..0ad309522a3e5 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/dns_query_tab_body.tsx @@ -5,11 +5,11 @@ * 2.0. */ -import React, { useEffect, useCallback, useMemo } from 'react'; +import React, { useEffect, useCallback, useMemo, useState } from 'react'; import { getOr } from 'lodash/fp'; import { NetworkDnsTable } from '../../components/network_dns_table'; -import { useNetworkDns } from '../../containers/network_dns'; +import { useNetworkDns, ID } from '../../containers/network_dns'; import { manageQuery } from '../../../common/components/page/manage_query'; import { NetworkComponentQueryProps } from './types'; @@ -24,6 +24,7 @@ import { MatrixHistogramType } from '../../../../common/search_strategy/security import { networkSelectors } from '../../store'; import { useShallowEqualSelector } from '../../../common/hooks/use_selector'; import { dnsTopDomainsLensAttributes } from '../../../common/components/visualization_actions/lens_attributes/network/dns_top_domains'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const HISTOGRAM_ID = 'networkDnsHistogramQuery'; @@ -72,6 +73,11 @@ const DnsQueryTabBodyComponent: React.FC = ({ }; }, [deleteQuery]); + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [ loading, { totalCount, networkDns, pageInfo, loadPage, id, inspect, isInspected, refetch }, @@ -80,7 +86,7 @@ const DnsQueryTabBodyComponent: React.FC = ({ endDate, filterQuery, indexNames, - skip, + skip: querySkip, startDate, type, }); @@ -122,6 +128,7 @@ const DnsQueryTabBodyComponent: React.FC = ({ loadPage={loadPage} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)} totalCount={totalCount} type={type} diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/http_query_tab_body.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/http_query_tab_body.tsx index bf9b0079650b2..98570a2f2f740 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/http_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/http_query_tab_body.tsx @@ -5,15 +5,16 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { getOr } from 'lodash/fp'; import { NetworkHttpTable } from '../../components/network_http_table'; -import { useNetworkHttp } from '../../containers/network_http'; +import { ID, useNetworkHttp } from '../../containers/network_http'; import { networkModel } from '../../store'; import { manageQuery } from '../../../common/components/page/manage_query'; import { HttpQueryTabBodyProps } from './types'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const NetworkHttpTableManage = manageQuery(NetworkHttpTable); @@ -25,6 +26,11 @@ export const HttpQueryTabBody = ({ startDate, setQuery, }: HttpQueryTabBodyProps) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [ loading, { id, inspect, isInspected, loadPage, networkHttp, pageInfo, refetch, totalCount }, @@ -32,7 +38,7 @@ export const HttpQueryTabBody = ({ endDate, filterQuery, indexNames, - skip, + skip: querySkip, startDate, type: networkModel.NetworkType.page, }); @@ -48,6 +54,7 @@ export const HttpQueryTabBody = ({ loadPage={loadPage} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)} totalCount={totalCount} type={networkModel.NetworkType.page} diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/ips_query_tab_body.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/ips_query_tab_body.tsx index aa21fe6066415..a497a35fe3551 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/ips_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/ips_query_tab_body.tsx @@ -5,15 +5,16 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { getOr } from 'lodash/fp'; import { NetworkTopNFlowTable } from '../../components/network_top_n_flow_table'; -import { useNetworkTopNFlow } from '../../containers/network_top_n_flow'; +import { ID, useNetworkTopNFlow } from '../../containers/network_top_n_flow'; import { networkModel } from '../../store'; import { manageQuery } from '../../../common/components/page/manage_query'; import { IPsQueryTabBodyProps } from './types'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const NetworkTopNFlowTableManage = manageQuery(NetworkTopNFlowTable); @@ -26,6 +27,11 @@ export const IPsQueryTabBody = ({ setQuery, flowTarget, }: IPsQueryTabBodyProps) => { + const { toggleStatus } = useQueryToggle(`${ID}-${flowTarget}`); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [ loading, { id, inspect, isInspected, loadPage, networkTopNFlow, pageInfo, refetch, totalCount }, @@ -34,7 +40,7 @@ export const IPsQueryTabBody = ({ flowTarget, filterQuery, indexNames, - skip, + skip: querySkip, startDate, type: networkModel.NetworkType.page, }); @@ -51,6 +57,7 @@ export const IPsQueryTabBody = ({ loadPage={loadPage} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)} totalCount={totalCount} type={networkModel.NetworkType.page} diff --git a/x-pack/plugins/security_solution/public/network/pages/navigation/tls_query_tab_body.tsx b/x-pack/plugins/security_solution/public/network/pages/navigation/tls_query_tab_body.tsx index 58c6f755b9175..c06a26f5d9192 100644 --- a/x-pack/plugins/security_solution/public/network/pages/navigation/tls_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/navigation/tls_query_tab_body.tsx @@ -5,12 +5,13 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { getOr } from 'lodash/fp'; import { manageQuery } from '../../../common/components/page/manage_query'; -import { useNetworkTls } from '../../../network/containers/tls'; +import { useNetworkTls, ID } from '../../containers/tls'; import { TlsTable } from '../../components/tls_table'; import { TlsQueryTabBodyProps } from './types'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const TlsTableManage = manageQuery(TlsTable); @@ -25,6 +26,11 @@ const TlsQueryTabBodyComponent: React.FC = ({ startDate, type, }) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [loading, { id, inspect, isInspected, tls, totalCount, pageInfo, loadPage, refetch }] = useNetworkTls({ endDate, @@ -32,7 +38,7 @@ const TlsQueryTabBodyComponent: React.FC = ({ flowTarget, indexNames, ip, - skip, + skip: querySkip, startDate, type, }); @@ -49,6 +55,7 @@ const TlsQueryTabBodyComponent: React.FC = ({ showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} totalCount={totalCount} type={type} /> diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx index 1295693db506f..173710a7700e8 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx @@ -21,9 +21,12 @@ import { import { OverviewHost } from '.'; import { createStore, State } from '../../../common/store'; import { useHostOverview } from '../../containers/overview_host'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; +import { render } from '@testing-library/react'; jest.mock('../../../common/lib/kibana'); jest.mock('../../../common/components/link_to'); +jest.mock('../../../common/containers/query_toggle'); const startDate = '2020-01-20T20:49:57.080Z'; const endDate = '2020-01-21T20:49:57.080Z'; @@ -32,6 +35,7 @@ const testProps = { indexNames: [], setQuery: jest.fn(), startDate, + filterQuery: '', }; const MOCKED_RESPONSE = { overviewHost: { @@ -56,7 +60,7 @@ const MOCKED_RESPONSE = { jest.mock('../../containers/overview_host'); const useHostOverviewMock = useHostOverview as jest.Mock; -useHostOverviewMock.mockReturnValue([false, MOCKED_RESPONSE]); +const mockUseQueryToggle = useQueryToggle as jest.Mock; describe('OverviewHost', () => { const state: State = mockGlobalState; @@ -65,7 +69,10 @@ describe('OverviewHost', () => { let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); beforeEach(() => { + jest.clearAllMocks(); + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); const myState = cloneDeep(state); + useHostOverviewMock.mockReturnValue([false, MOCKED_RESPONSE]); store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); }); @@ -103,4 +110,24 @@ describe('OverviewHost', () => { 'Showing: 16 events' ); }); + + test('toggleStatus=true, do not skip', () => { + const { queryByTestId } = render( + + + + ); + expect(useHostOverviewMock.mock.calls[0][0].skip).toEqual(false); + expect(queryByTestId('overview-hosts-stats')).toBeInTheDocument(); + }); + test('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + const { queryByTestId } = render( + + + + ); + expect(useHostOverviewMock.mock.calls[0][0].skip).toEqual(true); + expect(queryByTestId('overview-hosts-stats')).not.toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx index 32585c8836cc3..1bf990b755f65 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx @@ -9,7 +9,7 @@ import { isEmpty } from 'lodash/fp'; import { EuiFlexItem, EuiPanel } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { FormattedMessage } from '@kbn/i18n-react'; -import React, { useMemo, useCallback } from 'react'; +import React, { useMemo, useCallback, useState, useEffect } from 'react'; import { DEFAULT_NUMBER_FORMAT, APP_UI_ID } from '../../../../common/constants'; import { ESQuery } from '../../../../common/typed_json'; @@ -23,6 +23,7 @@ import { InspectButtonContainer } from '../../../common/components/inspect'; import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; import { SecurityPageName } from '../../../app/types'; import { LinkButton } from '../../../common/components/links'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; export interface OwnProps { startDate: GlobalTimeArgs['from']; @@ -46,12 +47,26 @@ const OverviewHostComponent: React.FC = ({ const { navigateToApp } = useKibana().services.application; const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); + const { toggleStatus, setToggleStatus } = useQueryToggle(OverviewHostQueryId); + const [querySkip, setQuerySkip] = useState(filterQuery === undefined || !toggleStatus); + useEffect(() => { + setQuerySkip(filterQuery === undefined || !toggleStatus); + }, [filterQuery, toggleStatus]); + const toggleQuery = useCallback( + (status: boolean) => { + setToggleStatus(status); + // toggle on = skipQuery false + setQuerySkip(!status); + }, + [setQuerySkip, setToggleStatus] + ); + const [loading, { overviewHost, id, inspect, refetch }] = useHostOverview({ endDate, filterQuery, indexNames, startDate, - skip: filterQuery === undefined, + skip: querySkip, }); const goToHost = useCallback( @@ -116,25 +131,29 @@ const OverviewHostComponent: React.FC = ({ return ( - + <>{hostPageButton} - - + {toggleStatus && ( + + )} diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx index dfc144be8e5bb..2293a0380f3a8 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx @@ -21,6 +21,8 @@ import { OverviewNetwork } from '.'; import { createStore, State } from '../../../common/store'; import { useNetworkOverview } from '../../containers/overview_network'; import { SecurityPageName } from '../../../app/types'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; +import { render } from '@testing-library/react'; jest.mock('../../../common/components/link_to'); const mockNavigateToApp = jest.fn(); @@ -46,6 +48,7 @@ const startDate = '2020-01-20T20:49:57.080Z'; const endDate = '2020-01-21T20:49:57.080Z'; const defaultProps = { endDate, + filterQuery: '', startDate, setQuery: jest.fn(), indexNames: [], @@ -65,9 +68,10 @@ const MOCKED_RESPONSE = { }, }; +jest.mock('../../../common/containers/query_toggle'); jest.mock('../../containers/overview_network'); const useNetworkOverviewMock = useNetworkOverview as jest.Mock; -useNetworkOverviewMock.mockReturnValue([false, MOCKED_RESPONSE]); +const mockUseQueryToggle = useQueryToggle as jest.Mock; describe('OverviewNetwork', () => { const state: State = mockGlobalState; @@ -76,6 +80,9 @@ describe('OverviewNetwork', () => { let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); beforeEach(() => { + jest.clearAllMocks(); + useNetworkOverviewMock.mockReturnValue([false, MOCKED_RESPONSE]); + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); const myState = cloneDeep(state); store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); }); @@ -143,4 +150,24 @@ describe('OverviewNetwork', () => { deepLinkId: SecurityPageName.network, }); }); + + it('toggleStatus=true, do not skip', () => { + const { queryByTestId } = render( + + + + ); + expect(useNetworkOverviewMock.mock.calls[0][0].skip).toEqual(false); + expect(queryByTestId('overview-network-stats')).toBeInTheDocument(); + }); + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + const { queryByTestId } = render( + + + + ); + expect(useNetworkOverviewMock.mock.calls[0][0].skip).toEqual(true); + expect(queryByTestId('overview-network-stats')).not.toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx index 7607a9eac4926..ce6c065d424d4 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx @@ -9,7 +9,7 @@ import { isEmpty } from 'lodash/fp'; import { EuiFlexItem, EuiPanel } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { FormattedMessage } from '@kbn/i18n-react'; -import React, { useMemo, useCallback } from 'react'; +import React, { useMemo, useCallback, useState, useEffect } from 'react'; import { DEFAULT_NUMBER_FORMAT, APP_UI_ID } from '../../../../common/constants'; import { ESQuery } from '../../../../common/typed_json'; @@ -26,6 +26,7 @@ import { InspectButtonContainer } from '../../../common/components/inspect'; import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; import { SecurityPageName } from '../../../app/types'; import { LinkButton } from '../../../common/components/links'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; export interface OverviewNetworkProps { startDate: GlobalTimeArgs['from']; @@ -48,12 +49,26 @@ const OverviewNetworkComponent: React.FC = ({ const { navigateToApp } = useKibana().services.application; const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); + const { toggleStatus, setToggleStatus } = useQueryToggle(OverviewNetworkQueryId); + const [querySkip, setQuerySkip] = useState(filterQuery === undefined || !toggleStatus); + useEffect(() => { + setQuerySkip(filterQuery === undefined || !toggleStatus); + }, [filterQuery, toggleStatus]); + const toggleQuery = useCallback( + (status: boolean) => { + setToggleStatus(status); + // toggle on = skipQuery false + setQuerySkip(!status); + }, + [setQuerySkip, setToggleStatus] + ); + const [loading, { overviewNetwork, id, inspect, refetch }] = useNetworkOverview({ endDate, filterQuery, indexNames, startDate, - skip: filterQuery === undefined, + skip: querySkip, }); const goToNetwork = useCallback( @@ -121,26 +136,30 @@ const OverviewNetworkComponent: React.FC = ({ return ( - + <> {networkPageButton} - - + {toggleStatus && ( + + )} diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.test.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.test.tsx new file mode 100644 index 0000000000000..53f07d5195c26 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.test.tsx @@ -0,0 +1,28 @@ +/* + * 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 { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../common/mock'; +import { useHostOverview } from './index'; + +describe('useHostOverview', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + skip: false, + }; + const { rerender } = renderHook(() => useHostOverview(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx index 52b58439af0ab..b79169b1ac762 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_host/index.tsx @@ -146,5 +146,12 @@ export const useHostOverview = ({ }; }, [overviewHostRequest, overviewHostSearch]); + useEffect(() => { + if (skip) { + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, overviewHostResponse]; }; diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.test.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.test.tsx new file mode 100644 index 0000000000000..64cc2e6bbd179 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.test.tsx @@ -0,0 +1,28 @@ +/* + * 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 { act, renderHook } from '@testing-library/react-hooks'; +import { TestProviders } from '../../../common/mock'; +import { useNetworkOverview } from './index'; + +describe('useNetworkOverview', () => { + it('skip = true will cancel any running request', () => { + const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); + const localProps = { + startDate: '2020-07-07T08:20:18.966Z', + endDate: '2020-07-08T08:20:18.966Z', + indexNames: ['cool'], + skip: false, + }; + const { rerender } = renderHook(() => useNetworkOverview(localProps), { + wrapper: TestProviders, + }); + localProps.skip = true; + act(() => rerender()); + expect(abortSpy).toHaveBeenCalledTimes(4); + }); +}); diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx index dd98a0ff03632..c2683b74a5b1a 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx @@ -147,5 +147,12 @@ export const useNetworkOverview = ({ }; }, [overviewNetworkRequest, overviewNetworkSearch]); + useEffect(() => { + if (skip) { + searchSubscription$.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, overviewNetworkResponse]; }; diff --git a/x-pack/plugins/security_solution/public/risk_score/containers/all/index.tsx b/x-pack/plugins/security_solution/public/risk_score/containers/all/index.tsx index b04d9dd05f283..8c95a081b3e86 100644 --- a/x-pack/plugins/security_solution/public/risk_score/containers/all/index.tsx +++ b/x-pack/plugins/security_solution/public/risk_score/containers/all/index.tsx @@ -266,5 +266,13 @@ export const useRiskScore = { + if (skip) { + setLoading(false); + searchSubscription.current.unsubscribe(); + abortCtrl.current.abort(); + } + }, [skip]); + return [loading, riskScoreResponse]; }; diff --git a/x-pack/plugins/security_solution/public/users/components/kpi_users/total_users/index.test.tsx b/x-pack/plugins/security_solution/public/users/components/kpi_users/total_users/index.test.tsx new file mode 100644 index 0000000000000..6425f40016fb9 --- /dev/null +++ b/x-pack/plugins/security_solution/public/users/components/kpi_users/total_users/index.test.tsx @@ -0,0 +1,68 @@ +/* + * 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 { useQueryToggle } from '../../../../common/containers/query_toggle'; +import { render } from '@testing-library/react'; +import { TestProviders } from '../../../../common/mock'; +import React from 'react'; +import { TotalUsersKpi } from './index'; +import { useSearchStrategy } from '../../../../common/containers/use_search_strategy'; + +jest.mock('../../../../common/containers/query_toggle'); +jest.mock('../../../../common/containers/use_search_strategy'); +jest.mock('../../../../hosts/components/kpi_hosts/common', () => ({ + KpiBaseComponentManage: () => , +})); + +describe('Total Users KPI', () => { + const mockUseSearchStrategy = useSearchStrategy as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + from: '2019-06-25T04:31:59.345Z', + to: '2019-06-25T06:31:59.345Z', + indexNames: [], + narrowDateRange: jest.fn(), + setQuery: jest.fn(), + skip: false, + }; + const mockSearch = jest.fn(); + beforeEach(() => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseSearchStrategy.mockReturnValue({ + result: [], + loading: false, + inspect: { + dsl: [], + response: [], + }, + search: mockSearch, + refetch: jest.fn(), + }); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(mockUseSearchStrategy.mock.calls[0][0].skip).toEqual(false); + expect(mockSearch).toHaveBeenCalled(); + }); + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + render( + + + + ); + expect(mockUseSearchStrategy.mock.calls[0][0].skip).toEqual(true); + expect(mockSearch).not.toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/users/components/kpi_users/total_users/index.tsx b/x-pack/plugins/security_solution/public/users/components/kpi_users/total_users/index.tsx index 043c6b472497e..ffa5d851875ce 100644 --- a/x-pack/plugins/security_solution/public/users/components/kpi_users/total_users/index.tsx +++ b/x-pack/plugins/security_solution/public/users/components/kpi_users/total_users/index.tsx @@ -6,7 +6,7 @@ */ import { euiPaletteColorBlind } from '@elastic/eui'; -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { UsersQueries } from '../../../../../common/search_strategy/security_solution/users'; import { UpdateDateRange } from '../../../../common/components/charts/common'; @@ -17,6 +17,7 @@ import { KpiBaseComponentManage } from '../../../../hosts/components/kpi_hosts/c import { kpiTotalUsersMetricLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/users/kpi_total_users_metric'; import { kpiTotalUsersAreaLensAttributes } from '../../../../common/components/visualization_actions/lens_attributes/users/kpi_total_users_area'; import * as i18n from './translations'; +import { useQueryToggle } from '../../../../common/containers/query_toggle'; const euiVisColorPalette = euiPaletteColorBlind(); const euiColorVis1 = euiVisColorPalette[1]; @@ -60,15 +61,21 @@ const TotalUsersKpiComponent: React.FC = ({ setQuery, skip, }) => { + const { toggleStatus } = useQueryToggle(UsersQueries.kpiTotalUsers); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const { loading, result, search, refetch, inspect } = useSearchStrategy({ factoryQueryType: UsersQueries.kpiTotalUsers, initialResult: { users: 0, usersHistogram: [] }, errorMessage: i18n.ERROR_USERS_KPI, + skip: querySkip, }); useEffect(() => { - if (!skip) { + if (!querySkip) { search({ filterQuery, defaultIndex: indexNames, @@ -79,7 +86,7 @@ const TotalUsersKpiComponent: React.FC = ({ }, }); } - }, [search, from, to, filterQuery, indexNames, skip]); + }, [search, from, to, filterQuery, indexNames, querySkip]); return ( = ({ narrowDateRange={narrowDateRange} refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} /> ); }; diff --git a/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/index.test.tsx b/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/index.test.tsx index 3faa96b436de0..c0cd2e351298e 100644 --- a/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/index.test.tsx @@ -14,7 +14,7 @@ import { UsersType } from '../../store/model'; describe('UserRiskScoreTable', () => { const username = 'test_user_name'; - const defautProps = { + const defaultProps = { data: [ { '@timestamp': '1641902481', @@ -32,6 +32,7 @@ describe('UserRiskScoreTable', () => { isInspect: false, loading: false, loadPage: noop, + setQuerySkip: jest.fn(), severityCount: { Unknown: 0, Low: 0, @@ -46,7 +47,7 @@ describe('UserRiskScoreTable', () => { it('renders', () => { const { queryByTestId } = render( - + ); diff --git a/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/index.tsx b/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/index.tsx index 9f782b7f28662..810525d4f1ca7 100644 --- a/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/index.tsx +++ b/x-pack/plugins/security_solution/public/users/components/user_risk_score_table/index.tsx @@ -57,6 +57,7 @@ interface UserRiskScoreTableProps { isInspect: boolean; loading: boolean; loadPage: (newActivePage: number) => void; + setQuerySkip: (skip: boolean) => void; severityCount: SeverityCount; totalCount: number; type: usersModel.UsersType; @@ -74,6 +75,7 @@ const UserRiskScoreTableComponent: React.FC = ({ isInspect, loading, loadPage, + setQuerySkip, severityCount, totalCount, type, @@ -210,6 +212,7 @@ const UserRiskScoreTableComponent: React.FC = ({ loadPage={loadPage} onChange={onSort} pageOfItems={data} + setQuerySkip={setQuerySkip} showMorePagesIndicator={false} sorting={sort} split={true} diff --git a/x-pack/plugins/security_solution/public/users/pages/navigation/all_users_query_tab_body.test.tsx b/x-pack/plugins/security_solution/public/users/pages/navigation/all_users_query_tab_body.test.tsx new file mode 100644 index 0000000000000..98b69d531c4dc --- /dev/null +++ b/x-pack/plugins/security_solution/public/users/pages/navigation/all_users_query_tab_body.test.tsx @@ -0,0 +1,68 @@ +/* + * 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 { TestProviders } from '../../../common/mock'; +import { useAuthentications } from '../../../hosts/containers/authentications'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; +import { AllUsersQueryTabBody } from './all_users_query_tab_body'; +import { UsersType } from '../../store/model'; + +jest.mock('../../../hosts/containers/authentications'); +jest.mock('../../../common/containers/query_toggle'); +jest.mock('../../../common/lib/kibana'); + +describe('All users query tab body', () => { + const mockUseAuthentications = useAuthentications as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + indexNames: [], + setQuery: jest.fn(), + skip: false, + startDate: '2019-06-25T04:31:59.345Z', + endDate: '2019-06-25T06:31:59.345Z', + type: UsersType.page, + }; + beforeEach(() => { + jest.clearAllMocks(); + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseAuthentications.mockReturnValue([ + false, + { + authentications: [], + id: '123', + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + totalCount: 0, + pageInfo: { activePage: 1, fakeTotalCount: 100, showMorePagesIndicator: false }, + loadPage: jest.fn(), + refetch: jest.fn(), + }, + ]); + }); + it('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(mockUseAuthentications.mock.calls[0][0].skip).toEqual(false); + }); + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + render( + + + + ); + expect(mockUseAuthentications.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/users/pages/navigation/all_users_query_tab_body.tsx b/x-pack/plugins/security_solution/public/users/pages/navigation/all_users_query_tab_body.tsx index 6c494c9752c4f..8fa963ef179f2 100644 --- a/x-pack/plugins/security_solution/public/users/pages/navigation/all_users_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/users/pages/navigation/all_users_query_tab_body.tsx @@ -6,12 +6,13 @@ */ import { getOr } from 'lodash/fp'; -import React from 'react'; -import { useAuthentications } from '../../../hosts/containers/authentications'; +import React, { useEffect, useState } from 'react'; +import { useAuthentications, ID } from '../../../hosts/containers/authentications'; import { UsersComponentsQueryProps } from './types'; import { AuthenticationTable } from '../../../hosts/components/authentications_table'; import { manageQuery } from '../../../common/components/page/manage_query'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const AuthenticationTableManage = manageQuery(AuthenticationTable); @@ -26,6 +27,11 @@ export const AllUsersQueryTabBody = ({ docValueFields, deleteQuery, }: UsersComponentsQueryProps) => { + const { toggleStatus } = useQueryToggle(ID); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); const [ loading, { authentications, totalCount, pageInfo, loadPage, id, inspect, isInspected, refetch }, @@ -34,7 +40,7 @@ export const AllUsersQueryTabBody = ({ endDate, filterQuery, indexNames, - skip, + skip: querySkip, startDate, // TODO Fix me // @ts-ignore @@ -55,6 +61,7 @@ export const AllUsersQueryTabBody = ({ refetch={refetch} showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)} setQuery={setQuery} + setQuerySkip={setQuerySkip} totalCount={totalCount} docValueFields={docValueFields} indexNames={indexNames} diff --git a/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_score_tab_body.test.tsx b/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_score_tab_body.test.tsx new file mode 100644 index 0000000000000..6b5ec66f864bb --- /dev/null +++ b/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_score_tab_body.test.tsx @@ -0,0 +1,81 @@ +/* + * 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 { TestProviders } from '../../../common/mock'; +import { useUserRiskScore, useUserRiskScoreKpi } from '../../../risk_score/containers'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; +import { UserRiskScoreQueryTabBody } from './user_risk_score_tab_body'; +import { UsersType } from '../../store/model'; + +jest.mock('../../../risk_score/containers'); +jest.mock('../../../common/containers/query_toggle'); +jest.mock('../../../common/lib/kibana'); + +describe('All users query tab body', () => { + const mockUseUserRiskScore = useUserRiskScore as jest.Mock; + const mockUseUserRiskScoreKpi = useUserRiskScoreKpi as jest.Mock; + const mockUseQueryToggle = useQueryToggle as jest.Mock; + const defaultProps = { + indexNames: [], + setQuery: jest.fn(), + skip: false, + startDate: '2019-06-25T04:31:59.345Z', + endDate: '2019-06-25T06:31:59.345Z', + type: UsersType.page, + }; + beforeEach(() => { + jest.clearAllMocks(); + mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); + mockUseUserRiskScore.mockReturnValue([ + false, + { + authentications: [], + id: '123', + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + totalCount: 0, + pageInfo: { activePage: 1, fakeTotalCount: 100, showMorePagesIndicator: false }, + loadPage: jest.fn(), + refetch: jest.fn(), + }, + ]); + mockUseUserRiskScoreKpi.mockReturnValue({ + loading: false, + severityCount: { + unknown: 12, + low: 12, + moderate: 12, + high: 12, + critical: 12, + }, + }); + }); + it('toggleStatus=true, do not skip', () => { + render( + + + + ); + expect(mockUseUserRiskScore.mock.calls[0][0].skip).toEqual(false); + expect(mockUseUserRiskScoreKpi.mock.calls[0][0].skip).toEqual(false); + }); + it('toggleStatus=false, skip', () => { + mockUseQueryToggle.mockReturnValue({ toggleStatus: false, setToggleStatus: jest.fn() }); + render( + + + + ); + expect(mockUseUserRiskScore.mock.calls[0][0].skip).toEqual(true); + expect(mockUseUserRiskScoreKpi.mock.calls[0][0].skip).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_score_tab_body.tsx b/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_score_tab_body.tsx index a19e7803cb90f..a479788ce0f41 100644 --- a/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_score_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/users/pages/navigation/user_risk_score_tab_body.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { noop } from 'lodash/fp'; import { UsersComponentsQueryProps } from './types'; @@ -20,6 +20,7 @@ import { useUserRiskScore, useUserRiskScoreKpi, } from '../../../risk_score/containers'; +import { useQueryToggle } from '../../../common/containers/query_toggle'; const UserRiskScoreTableManage = manageQuery(UserRiskScoreTable); @@ -43,15 +44,22 @@ export const UserRiskScoreQueryTabBody = ({ [activePage, limit] ); + const { toggleStatus } = useQueryToggle(UserRiskScoreQueryId.USERS_BY_RISK); + const [querySkip, setQuerySkip] = useState(skip || !toggleStatus); + useEffect(() => { + setQuerySkip(skip || !toggleStatus); + }, [skip, toggleStatus]); + const [loading, { data, totalCount, inspect, isInspected, refetch }] = useUserRiskScore({ filterQuery, - skip, + skip: querySkip, pagination, sort, }); const { severityCount, loading: isKpiLoading } = useUserRiskScoreKpi({ filterQuery, + skip: querySkip, }); return ( @@ -65,6 +73,7 @@ export const UserRiskScoreQueryTabBody = ({ loadPage={noop} // It isn't necessary because PaginatedTable updates redux store and we load the page when activePage updates on the store refetch={refetch} setQuery={setQuery} + setQuerySkip={setQuerySkip} severityCount={severityCount} totalCount={totalCount} type={type} From 7aa89aac3bb5f2ba972aa4349259c53845becdbb Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Wed, 23 Mar 2022 11:52:52 -0700 Subject: [PATCH 09/13] Fix typos in dev docs (#128400) --- dev_docs/contributing/standards.mdx | 2 +- dev_docs/key_concepts/kibana_platform_plugin_intro.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dev_docs/contributing/standards.mdx b/dev_docs/contributing/standards.mdx index d2f31f3a4faa2..cef9199aee924 100644 --- a/dev_docs/contributing/standards.mdx +++ b/dev_docs/contributing/standards.mdx @@ -69,7 +69,7 @@ Every team should be collecting telemetry metrics on it’s public API usage. Th ### APM -Kibana server and client are instrumented with APM node and APM RUM clients respectively, tracking serveral types of transactions by default, such as `page-load`, `request`, etc. +Kibana server and client are instrumented with APM node and APM RUM clients respectively, tracking several types of transactions by default, such as `page-load`, `request`, etc. You may introduce custom transactions. Please refer to the [APM documentation](https://www.elastic.co/guide/en/apm/get-started/current/index.html) and follow these guidelines when doing so: - Use dashed syntax for transaction types and names: `my-transaction-type` and `my-transaction-name` diff --git a/dev_docs/key_concepts/kibana_platform_plugin_intro.mdx b/dev_docs/key_concepts/kibana_platform_plugin_intro.mdx index 195e5c1f6f211..417d6e4983d4f 100644 --- a/dev_docs/key_concepts/kibana_platform_plugin_intro.mdx +++ b/dev_docs/key_concepts/kibana_platform_plugin_intro.mdx @@ -153,7 +153,7 @@ plugins to customize the Kibana experience. Examples of extension points are: - core.overlays.showModal - embeddables.registerEmbeddableFactory - uiActions.registerAction -- core.saedObjects.registerType +- core.savedObjects.registerType ## Follow up material From 55e42cec93228a447b624c2b0001696712257efe Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Wed, 23 Mar 2022 20:26:00 +0100 Subject: [PATCH 10/13] [Cases] Allow custom toast title and content in cases hooks (#128145) --- .../cases/public/common/translations.ts | 15 +- .../public/common/use_cases_toast.test.tsx | 135 +++++++++++++++--- .../cases/public/common/use_cases_toast.tsx | 87 +++++++++-- .../use_cases_add_to_existing_case_modal.tsx | 16 ++- .../use_cases_add_to_new_case_flyout.tsx | 14 +- 5 files changed, 230 insertions(+), 37 deletions(-) diff --git a/x-pack/plugins/cases/public/common/translations.ts b/x-pack/plugins/cases/public/common/translations.ts index 5c349a65dd869..10005b2c87bce 100644 --- a/x-pack/plugins/cases/public/common/translations.ts +++ b/x-pack/plugins/cases/public/common/translations.ts @@ -257,13 +257,22 @@ export const LINK_APPROPRIATE_LICENSE = i18n.translate('xpack.cases.common.appro export const CASE_SUCCESS_TOAST = (title: string) => i18n.translate('xpack.cases.actions.caseSuccessToast', { + values: { title }, + defaultMessage: '{title} has been updated', + }); + +export const CASE_ALERT_SUCCESS_TOAST = (title: string) => + i18n.translate('xpack.cases.actions.caseAlertSuccessToast', { values: { title }, defaultMessage: 'An alert has been added to "{title}"', }); -export const CASE_SUCCESS_SYNC_TEXT = i18n.translate('xpack.cases.actions.caseSuccessSyncText', { - defaultMessage: 'Alerts in this case have their status synched with the case status', -}); +export const CASE_ALERT_SUCCESS_SYNC_TEXT = i18n.translate( + 'xpack.cases.actions.caseAlertSuccessSyncText', + { + defaultMessage: 'Alerts in this case have their status synched with the case status', + } +); export const VIEW_CASE = i18n.translate('xpack.cases.actions.viewCase', { defaultMessage: 'View Case', diff --git a/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx b/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx index 9bd6a6675a5c1..517d1cfdd77b1 100644 --- a/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx +++ b/x-pack/plugins/cases/public/common/use_cases_toast.test.tsx @@ -9,33 +9,97 @@ import { renderHook } from '@testing-library/react-hooks'; import { useToasts } from '../common/lib/kibana'; import { AppMockRenderer, createAppMockRenderer, TestProviders } from '../common/mock'; import { CaseToastSuccessContent, useCasesToast } from './use_cases_toast'; -import { mockCase } from '../containers/mock'; +import { alertComment, basicComment, mockCase } from '../containers/mock'; import React from 'react'; import userEvent from '@testing-library/user-event'; +import { SupportedCaseAttachment } from '../types'; jest.mock('../common/lib/kibana'); const useToastsMock = useToasts as jest.Mock; describe('Use cases toast hook', () => { + const successMock = jest.fn(); + + function validateTitle(title: string) { + const mockParams = successMock.mock.calls[0][0]; + const el = document.createElement('div'); + mockParams.title(el); + expect(el).toHaveTextContent(title); + } + + function validateContent(content: string) { + const mockParams = successMock.mock.calls[0][0]; + const el = document.createElement('div'); + mockParams.text(el); + expect(el).toHaveTextContent(content); + } + + useToastsMock.mockImplementation(() => { + return { + addSuccess: successMock, + }; + }); + + beforeEach(() => { + successMock.mockClear(); + }); + describe('Toast hook', () => { - const successMock = jest.fn(); - useToastsMock.mockImplementation(() => { - return { - addSuccess: successMock, - }; - }); - it('should create a success tost when invoked with a case', () => { + it('should create a success toast when invoked with a case', () => { const { result } = renderHook( () => { return useCasesToast(); }, { wrapper: TestProviders } ); - result.current.showSuccessAttach(mockCase); + result.current.showSuccessAttach({ + theCase: mockCase, + }); expect(successMock).toHaveBeenCalled(); }); }); + + describe('toast title', () => { + it('should create a success toast when invoked with a case and a custom title', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + result.current.showSuccessAttach({ theCase: mockCase, title: 'Custom title' }); + validateTitle('Custom title'); + }); + + it('should display the alert sync title when called with an alert attachment ', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + result.current.showSuccessAttach({ + theCase: mockCase, + attachments: [alertComment as SupportedCaseAttachment], + }); + validateTitle('An alert has been added to "Another horrible breach!!'); + }); + + it('should display a generic title when called with a non-alert attachament', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + result.current.showSuccessAttach({ + theCase: mockCase, + attachments: [basicComment as SupportedCaseAttachment], + }); + validateTitle('Another horrible breach!! has been updated'); + }); + }); describe('Toast content', () => { let appMockRender: AppMockRenderer; const onViewCaseClick = jest.fn(); @@ -44,20 +108,57 @@ describe('Use cases toast hook', () => { onViewCaseClick.mockReset(); }); - it('renders a correct successfull message with synced alerts', () => { - const result = appMockRender.render( - + it('should create a success toast when invoked with a case and a custom content', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } ); - expect(result.getByTestId('toaster-content-sync-text')).toHaveTextContent( - 'Alerts in this case have their status synched with the case status' + result.current.showSuccessAttach({ theCase: mockCase, content: 'Custom content' }); + validateContent('Custom content'); + }); + + it('renders an alert-specific content when called with an alert attachment and sync on', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + result.current.showSuccessAttach({ + theCase: mockCase, + attachments: [alertComment as SupportedCaseAttachment], + }); + validateContent('Alerts in this case have their status synched with the case status'); + }); + + it('renders empty content when called with an alert attachment and sync off', () => { + const { result } = renderHook( + () => { + return useCasesToast(); + }, + { wrapper: TestProviders } + ); + result.current.showSuccessAttach({ + theCase: { ...mockCase, settings: { ...mockCase.settings, syncAlerts: false } }, + attachments: [alertComment as SupportedCaseAttachment], + }); + validateContent('View Case'); + }); + + it('renders a correct successful message content', () => { + const result = appMockRender.render( + ); + expect(result.getByTestId('toaster-content-sync-text')).toHaveTextContent('my content'); expect(result.getByTestId('toaster-content-case-view-link')).toHaveTextContent('View Case'); expect(onViewCaseClick).not.toHaveBeenCalled(); }); - it('renders a correct successfull message with not synced alerts', () => { + it('renders a correct successful message without content', () => { const result = appMockRender.render( - + ); expect(result.queryByTestId('toaster-content-sync-text')).toBeFalsy(); expect(result.getByTestId('toaster-content-case-view-link')).toHaveTextContent('View Case'); @@ -66,7 +167,7 @@ describe('Use cases toast hook', () => { it('Calls the onViewCaseClick when clicked', () => { const result = appMockRender.render( - + ); userEvent.click(result.getByTestId('toaster-content-case-view-link')); expect(onViewCaseClick).toHaveBeenCalled(); diff --git a/x-pack/plugins/cases/public/common/use_cases_toast.tsx b/x-pack/plugins/cases/public/common/use_cases_toast.tsx index 98cc7fa1d8faa..d02f792d601cf 100644 --- a/x-pack/plugins/cases/public/common/use_cases_toast.tsx +++ b/x-pack/plugins/cases/public/common/use_cases_toast.tsx @@ -9,10 +9,16 @@ import { EuiButtonEmpty, EuiText } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; -import { Case } from '../../common'; +import { Case, CommentType } from '../../common'; import { useToasts } from '../common/lib/kibana'; import { useCaseViewNavigation } from '../common/navigation'; -import { CASE_SUCCESS_SYNC_TEXT, CASE_SUCCESS_TOAST, VIEW_CASE } from './translations'; +import { CaseAttachments } from '../types'; +import { + CASE_ALERT_SUCCESS_SYNC_TEXT, + CASE_ALERT_SUCCESS_TOAST, + CASE_SUCCESS_TOAST, + VIEW_CASE, +} from './translations'; const LINE_CLAMP = 3; const Title = styled.span` @@ -28,46 +34,101 @@ const EuiTextStyled = styled(EuiText)` `} `; +function getToastTitle({ + theCase, + title, + attachments, +}: { + theCase: Case; + title?: string; + attachments?: CaseAttachments; +}): string { + if (title !== undefined) { + return title; + } + if (attachments !== undefined) { + for (const attachment of attachments) { + if (attachment.type === CommentType.alert) { + return CASE_ALERT_SUCCESS_TOAST(theCase.title); + } + } + } + return CASE_SUCCESS_TOAST(theCase.title); +} + +function getToastContent({ + theCase, + content, + attachments, +}: { + theCase: Case; + content?: string; + attachments?: CaseAttachments; +}): string | undefined { + if (content !== undefined) { + return content; + } + if (attachments !== undefined) { + for (const attachment of attachments) { + if (attachment.type === CommentType.alert && theCase.settings.syncAlerts) { + return CASE_ALERT_SUCCESS_SYNC_TEXT; + } + } + } + return undefined; +} + export const useCasesToast = () => { const { navigateToCaseView } = useCaseViewNavigation(); const toasts = useToasts(); return { - showSuccessAttach: (theCase: Case) => { + showSuccessAttach: ({ + theCase, + attachments, + title, + content, + }: { + theCase: Case; + attachments?: CaseAttachments; + title?: string; + content?: string; + }) => { const onViewCaseClick = () => { navigateToCaseView({ detailName: theCase.id, }); }; + const renderTitle = getToastTitle({ theCase, title, attachments }); + const renderContent = getToastContent({ theCase, content, attachments }); + return toasts.addSuccess({ color: 'success', iconType: 'check', - title: toMountPoint({CASE_SUCCESS_TOAST(theCase.title)}), + title: toMountPoint({renderTitle}), text: toMountPoint( - + ), }); }, }; }; + export const CaseToastSuccessContent = ({ - syncAlerts, onViewCaseClick, + content, }: { - syncAlerts: boolean; onViewCaseClick: () => void; + content?: string; }) => { return ( <> - {syncAlerts && ( + {content !== undefined ? ( - {CASE_SUCCESS_SYNC_TEXT} + {content} - )} + ) : null} { +type AddToExistingFlyoutProps = AllCasesSelectorModalProps & { + toastTitle?: string; + toastContent?: string; +}; + +export const useCasesAddToExistingCaseModal = (props: AddToExistingFlyoutProps) => { const createNewCaseFlyout = useCasesAddToNewCaseFlyout({ attachments: props.attachments, onClose: props.onClose, @@ -25,6 +30,8 @@ export const useCasesAddToExistingCaseModal = (props: AllCasesSelectorModalProps return props.onRowClick(theCase); } }, + toastTitle: props.toastTitle, + toastContent: props.toastContent, }); const { dispatch } = useCasesContext(); const casesToasts = useCasesToast(); @@ -53,7 +60,12 @@ export const useCasesAddToExistingCaseModal = (props: AllCasesSelectorModalProps closeModal(); createNewCaseFlyout.open(); } else { - casesToasts.showSuccessAttach(theCase); + casesToasts.showSuccessAttach({ + theCase, + attachments: props.attachments, + title: props.toastTitle, + content: props.toastContent, + }); if (props.onRowClick) { props.onRowClick(theCase); } diff --git a/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.tsx b/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.tsx index 5422ab9be995d..c1c0793fe2340 100644 --- a/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.tsx +++ b/x-pack/plugins/cases/public/components/create/flyout/use_cases_add_to_new_case_flyout.tsx @@ -12,7 +12,12 @@ import { CasesContextStoreActionsList } from '../../cases_context/cases_context_ import { useCasesContext } from '../../cases_context/use_cases_context'; import { CreateCaseFlyoutProps } from './create_case_flyout'; -export const useCasesAddToNewCaseFlyout = (props: CreateCaseFlyoutProps) => { +type AddToNewCaseFlyoutProps = CreateCaseFlyoutProps & { + toastTitle?: string; + toastContent?: string; +}; + +export const useCasesAddToNewCaseFlyout = (props: AddToNewCaseFlyoutProps) => { const { dispatch } = useCasesContext(); const casesToasts = useCasesToast(); @@ -35,7 +40,12 @@ export const useCasesAddToNewCaseFlyout = (props: CreateCaseFlyoutProps) => { }, onSuccess: async (theCase: Case) => { if (theCase) { - casesToasts.showSuccessAttach(theCase); + casesToasts.showSuccessAttach({ + theCase, + attachments: props.attachments, + title: props.toastTitle, + content: props.toastContent, + }); } if (props.onSuccess) { return props.onSuccess(theCase); From bbacf5e55a4e0f2e057deb614f15e55fd6a5e127 Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Thu, 24 Mar 2022 07:04:39 +0100 Subject: [PATCH 11/13] Fix invalid URL path --- .../src/solution_avatar/solution_avatar.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kbn-shared-ux-components/src/solution_avatar/solution_avatar.scss b/packages/kbn-shared-ux-components/src/solution_avatar/solution_avatar.scss index 1402f272a15cf..68bbaf6fe7249 100644 --- a/packages/kbn-shared-ux-components/src/solution_avatar/solution_avatar.scss +++ b/packages/kbn-shared-ux-components/src/solution_avatar/solution_avatar.scss @@ -7,7 +7,7 @@ line-height: 100px; border-radius: 100px; display: inline-block; - background: $euiColorEmptyShade url('assets/texture.svg') no-repeat; + background: $euiColorEmptyShade url('./assets/texture.svg') no-repeat; background-size: cover, 125%; text-align: center; } From 1c616d59d89fbaa22a4d69111945de43c5671683 Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Thu, 24 Mar 2022 09:05:00 +0100 Subject: [PATCH 12/13] Fix url path --- .../src/solution_avatar/solution_avatar.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kbn-shared-ux-components/src/solution_avatar/solution_avatar.scss b/packages/kbn-shared-ux-components/src/solution_avatar/solution_avatar.scss index 68bbaf6fe7249..1402f272a15cf 100644 --- a/packages/kbn-shared-ux-components/src/solution_avatar/solution_avatar.scss +++ b/packages/kbn-shared-ux-components/src/solution_avatar/solution_avatar.scss @@ -7,7 +7,7 @@ line-height: 100px; border-radius: 100px; display: inline-block; - background: $euiColorEmptyShade url('./assets/texture.svg') no-repeat; + background: $euiColorEmptyShade url('assets/texture.svg') no-repeat; background-size: cover, 125%; text-align: center; } From ec00f9a825d0a2dacd878975ca554ee2cbce3ed6 Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Thu, 24 Mar 2022 10:03:36 +0100 Subject: [PATCH 13/13] Fix url path --- .../src/solution_avatar/solution_avatar.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kbn-shared-ux-components/src/solution_avatar/solution_avatar.scss b/packages/kbn-shared-ux-components/src/solution_avatar/solution_avatar.scss index 1402f272a15cf..3064ef0a04a67 100644 --- a/packages/kbn-shared-ux-components/src/solution_avatar/solution_avatar.scss +++ b/packages/kbn-shared-ux-components/src/solution_avatar/solution_avatar.scss @@ -7,7 +7,7 @@ line-height: 100px; border-radius: 100px; display: inline-block; - background: $euiColorEmptyShade url('assets/texture.svg') no-repeat; + background: $euiColorEmptyShade url('/assets/texture.svg') no-repeat; background-size: cover, 125%; text-align: center; }