diff --git a/x-pack/plugins/security_solution/public/common/components/inspect/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/inspect/index.test.tsx index d73b4cb7d98d6..b3dbbb86ace68 100644 --- a/x-pack/plugins/security_solution/public/common/components/inspect/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/inspect/index.test.tsx @@ -21,6 +21,10 @@ import { UpdateQueryParams, upsertQuery } from '../../store/inputs/helpers'; import { InspectButton } from '.'; import { cloneDeep } from 'lodash/fp'; +jest.mock('./modal', () => ({ + ModalInspectQuery: jest.fn(() =>
), +})); + describe('Inspect Button', () => { const refetch = jest.fn(); const state: State = mockGlobalState; @@ -103,6 +107,54 @@ describe('Inspect Button', () => { ); expect(wrapper.find('.euiButtonIcon').get(0).props.disabled).toBe(true); }); + + test('Button disabled when inspect == null', () => { + const myState = cloneDeep(state); + const myQuery = cloneDeep(newQuery); + myQuery.inspect = null; + myState.inputs = upsertQuery(myQuery); + store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + const wrapper = mount( + + + + ); + expect(wrapper.find('.euiButtonIcon').get(0).props.disabled).toBe(true); + }); + + test('Button disabled when inspect.dsl.length == 0', () => { + const myState = cloneDeep(state); + const myQuery = cloneDeep(newQuery); + myQuery.inspect = { + dsl: [], + response: ['my response'], + }; + myState.inputs = upsertQuery(myQuery); + store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + const wrapper = mount( + + + + ); + expect(wrapper.find('.euiButtonIcon').get(0).props.disabled).toBe(true); + }); + + test('Button disabled when inspect.response.length == 0', () => { + const myState = cloneDeep(state); + const myQuery = cloneDeep(newQuery); + myQuery.inspect = { + dsl: ['my dsl'], + response: [], + }; + myState.inputs = upsertQuery(myQuery); + store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + const wrapper = mount( + + + + ); + expect(wrapper.find('.euiButtonIcon').get(0).props.disabled).toBe(true); + }); }); describe('Modal Inspect - happy path', () => { @@ -127,46 +179,103 @@ describe('Inspect Button', () => { wrapper.update(); expect(store.getState().inputs.global.queries[0].isInspected).toBe(true); - expect(wrapper.find('button[data-test-subj="modal-inspect-close"]').first().exists()).toBe( - true - ); + expect(wrapper.find('[data-test-subj="mocker-modal"]').first().exists()).toBe(true); }); - test('Close Inspect Modal', () => { + test('Do not Open Inspect Modal if it is loading', () => { const wrapper = mount( ); + expect(store.getState().inputs.global.queries[0].isInspected).toBe(false); + store.getState().inputs.global.queries[0].loading = true; wrapper.find('button[data-test-subj="inspect-icon-button"]').first().simulate('click'); wrapper.update(); - wrapper.find('button[data-test-subj="modal-inspect-close"]').first().simulate('click'); - - wrapper.update(); - - expect(store.getState().inputs.global.queries[0].isInspected).toBe(false); + expect(store.getState().inputs.global.queries[0].isInspected).toBe(true); expect(wrapper.find('button[data-test-subj="modal-inspect-close"]').first().exists()).toBe( false ); }); + }); - test('Do not Open Inspect Modal if it is loading', () => { + describe('Modal Inspect - show or hide', () => { + test('shows when request/response are complete and isInspected=true', () => { + const myState = cloneDeep(state); + const myQuery = cloneDeep(newQuery); + myQuery.inspect = { + dsl: ['a length'], + response: ['my response'], + }; + myState.inputs = upsertQuery(myQuery); + myState.inputs.global.queries[0].isInspected = true; + store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); const wrapper = mount( ); - store.getState().inputs.global.queries[0].loading = true; - wrapper.find('button[data-test-subj="inspect-icon-button"]').first().simulate('click'); - wrapper.update(); + expect(wrapper.find('[data-test-subj="mocker-modal"]').first().exists()).toEqual(true); + }); - expect(store.getState().inputs.global.queries[0].isInspected).toBe(true); - expect(wrapper.find('button[data-test-subj="modal-inspect-close"]').first().exists()).toBe( - false + test('hides when request/response are complete and isInspected=false', () => { + const myState = cloneDeep(state); + const myQuery = cloneDeep(newQuery); + myQuery.inspect = { + dsl: ['a length'], + response: ['my response'], + }; + myState.inputs = upsertQuery(myQuery); + myState.inputs.global.queries[0].isInspected = false; + store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="mocker-modal"]').first().exists()).toEqual(false); + }); + + test('hides when request is empty and isInspected=true', () => { + const myState = cloneDeep(state); + const myQuery = cloneDeep(newQuery); + myQuery.inspect = { + dsl: [], + response: ['my response'], + }; + myState.inputs = upsertQuery(myQuery); + myState.inputs.global.queries[0].isInspected = true; + store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="mocker-modal"]').first().exists()).toEqual(false); + }); + + test('hides when response is empty and isInspected=true', () => { + const myState = cloneDeep(state); + const myQuery = cloneDeep(newQuery); + myQuery.inspect = { + dsl: ['my dsl'], + response: [], + }; + myState.inputs = upsertQuery(myQuery); + myState.inputs.global.queries[0].isInspected = true; + store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + const wrapper = mount( + + + ); + + expect(wrapper.find('[data-test-subj="mocker-modal"]').first().exists()).toEqual(false); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx b/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx index 4f52703620b5f..defb90b9054f1 100644 --- a/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/inspect/index.tsx @@ -7,7 +7,7 @@ import { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui'; import { omit } from 'lodash/fp'; -import React, { useCallback } from 'react'; +import React, { useMemo, useCallback } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { inputsSelectors, State } from '../../store'; @@ -52,10 +52,10 @@ const InspectButtonComponent: React.FC = ({ compact = false, inputId = 'global', inspect, + inspectIndex = 0, isDisabled, isInspected, loading, - inspectIndex = 0, multiple = false, // If multiple = true we ignore the inspectIndex and pass all requests and responses to the inspect modal onCloseInspect, queryId = '', @@ -63,7 +63,6 @@ const InspectButtonComponent: React.FC = ({ setIsInspected, title = '', }) => { - const isShowingModal = !loading && selectedInspectIndex === inspectIndex && isInspected; const handleClick = useCallback(() => { setIsInspected({ id: queryId, @@ -105,6 +104,16 @@ const InspectButtonComponent: React.FC = ({ } } + const isShowingModal = useMemo( + () => !loading && selectedInspectIndex === inspectIndex && isInspected, + [inspectIndex, isInspected, loading, selectedInspectIndex] + ); + + const isButtonDisabled = useMemo( + () => loading || isDisabled || request == null || response == null, + [isDisabled, loading, request, response] + ); + return ( <> {inputId === 'timeline' && !compact && ( @@ -115,7 +124,7 @@ const InspectButtonComponent: React.FC = ({ color="text" iconSide="left" iconType="inspect" - isDisabled={loading || isDisabled || false} + isDisabled={isButtonDisabled} isLoading={loading} onClick={handleClick} > @@ -129,21 +138,23 @@ const InspectButtonComponent: React.FC = ({ data-test-subj="inspect-icon-button" iconSize="m" iconType="inspect" - isDisabled={loading || isDisabled || false} + isDisabled={isButtonDisabled} title={i18n.INSPECT} onClick={handleClick} /> )} - + {isShowingModal && request !== null && response !== null && ( + + )} ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/inspect/modal.test.tsx b/x-pack/plugins/security_solution/public/common/components/inspect/modal.test.tsx index 572513180025f..7a9c36a986afd 100644 --- a/x-pack/plugins/security_solution/public/common/components/inspect/modal.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/inspect/modal.test.tsx @@ -7,103 +7,50 @@ import { mount } from 'enzyme'; import React from 'react'; -import { ThemeProvider } from 'styled-components'; +import { TestProviders } from '../../mock'; import { NO_ALERT_INDEX } from '../../../../common/constants'; import { ModalInspectQuery, formatIndexPatternRequested } from './modal'; -import { getMockTheme } from '../../lib/kibana/kibana_react.mock'; +import { InputsModelId } from '../../store/inputs/constants'; +import { EXCLUDE_ELASTIC_CLOUD_INDEX } from '../../containers/sourcerer'; -const mockTheme = getMockTheme({ - eui: { - euiBreakpoints: { - l: '1200px', - }, - }, +jest.mock('react-router-dom', () => { + const original = jest.requireActual('react-router-dom'); + + return { + ...original, + useLocation: jest.fn().mockReturnValue([{ pathname: '/overview' }]), + }; }); -const request = - '{"index": ["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"allowNoIndices": true, "ignoreUnavailable": true, "body": { "aggregations": {"hosts": {"cardinality": {"field": "host.name" } }, "hosts_histogram": {"auto_date_histogram": {"field": "@timestamp","buckets": "6"},"aggs": { "count": {"cardinality": {"field": "host.name" }}}}}, "query": {"bool": {"filter": [{"range": { "@timestamp": {"gte": 1562290224506,"lte": 1562376624506 }}}]}}, "size": 0, "track_total_hits": false}}'; +const getRequest = ( + indices: string[] = ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'] +) => + `{"index": ${JSON.stringify( + indices + )},"allowNoIndices": true, "ignoreUnavailable": true, "body": { "aggregations": {"hosts": {"cardinality": {"field": "host.name" } }, "hosts_histogram": {"auto_date_histogram": {"field": "@timestamp","buckets": "6"},"aggs": { "count": {"cardinality": {"field": "host.name" }}}}}, "query": {"bool": {"filter": [{"range": { "@timestamp": {"gte": 1562290224506,"lte": 1562376624506 }}}]}}, "size": 0, "track_total_hits": false}}`; + +const request = getRequest(); + const response = '{"took": 880,"timed_out": false,"_shards": {"total": 26,"successful": 26,"skipped": 0,"failed": 0},"hits": {"max_score": null,"hits": []},"aggregations": {"hosts": {"value": 541},"hosts_histogram": {"buckets": [{"key_as_string": "2019 - 07 - 05T01: 00: 00.000Z", "key": 1562288400000, "doc_count": 1492321, "count": { "value": 105 }}, {"key_as_string": "2019 - 07 - 05T13: 00: 00.000Z", "key": 1562331600000, "doc_count": 2412761, "count": { "value": 453}},{"key_as_string": "2019 - 07 - 06T01: 00: 00.000Z", "key": 1562374800000, "doc_count": 111658, "count": { "value": 15}}],"interval": "12h"}},"status": 200}'; describe('Modal Inspect', () => { const closeModal = jest.fn(); - - describe('rendering', () => { - test('when isShowing is positive and request and response are not null', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find('[data-test-subj="modal-inspect-euiModal"]').first().exists()).toBe(true); - expect(wrapper.find('.euiModalHeader__title').first().text()).toBe('Inspect My title'); - }); - - test('when isShowing is negative and request and response are not null', () => { - const wrapper = mount( - - ); - expect(wrapper.find('[data-test-subj="modal-inspect-euiModal"]').first().exists()).toBe( - false - ); - }); - - test('when isShowing is positive and request is null and response is not null', () => { - const wrapper = mount( - - ); - expect(wrapper.find('[data-test-subj="modal-inspect-euiModal"]').first().exists()).toBe( - false - ); - }); - - test('when isShowing is positive and request is not null and response is null', () => { - const wrapper = mount( - - ); - expect(wrapper.find('[data-test-subj="modal-inspect-euiModal"]').first().exists()).toBe( - false - ); - }); - }); + const defaultProps = { + closeModal, + inputId: 'timeline' as InputsModelId, + request, + response, + title: 'My title', + }; describe('functionality from tab statistics/request/response', () => { test('Click on statistic Tab', () => { const wrapper = mount( - - - + + + ); wrapper.find('.euiTab').first().simulate('click'); @@ -134,15 +81,9 @@ describe('Modal Inspect', () => { test('Click on request Tab', () => { const wrapper = mount( - - - + + + ); wrapper.find('.euiTab').at(2).simulate('click'); @@ -201,15 +142,9 @@ describe('Modal Inspect', () => { test('Click on response Tab', () => { const wrapper = mount( - - - + + + ); wrapper.find('.euiTab').at(1).simulate('click'); @@ -237,15 +172,9 @@ describe('Modal Inspect', () => { describe('events', () => { test('Make sure that toggle function has been called when you click on the close button', () => { const wrapper = mount( - - - + + + ); wrapper.find('button[data-test-subj="modal-inspect-close"]').simulate('click'); @@ -280,4 +209,37 @@ describe('Modal Inspect', () => { expect(expected).toEqual('Sorry about that, something went wrong.'); }); }); + + describe('index pattern messaging', () => { + test('no messaging when all patterns are in sourcerer selection', () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('i[data-test-subj="not-sourcerer-msg"]').first().exists()).toEqual(false); + expect(wrapper.find('i[data-test-subj="exclude-logs-msg"]').first().exists()).toEqual(false); + }); + test('not-sourcerer-msg when not all patterns are in sourcerer selection', () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('i[data-test-subj="not-sourcerer-msg"]').first().exists()).toEqual(true); + expect(wrapper.find('i[data-test-subj="exclude-logs-msg"]').first().exists()).toEqual(false); + }); + test('exclude-logs-msg when EXCLUDE_ELASTIC_CLOUD_INDEX is present in patterns', () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('i[data-test-subj="not-sourcerer-msg"]').first().exists()).toEqual(false); + expect(wrapper.find('i[data-test-subj="exclude-logs-msg"]').first().exists()).toEqual(true); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/inspect/modal.tsx b/x-pack/plugins/security_solution/public/common/components/inspect/modal.tsx index 78a3d744e46bb..45fcf1e746b87 100644 --- a/x-pack/plugins/security_solution/public/common/components/inspect/modal.tsx +++ b/x-pack/plugins/security_solution/public/common/components/inspect/modal.tsx @@ -19,11 +19,19 @@ import { EuiTabbedContent, } from '@elastic/eui'; import numeral from '@elastic/numeral'; -import React, { Fragment, ReactNode } from 'react'; +import React, { useMemo, Fragment, ReactNode } from 'react'; import styled from 'styled-components'; +import { useLocation } from 'react-router-dom'; import { NO_ALERT_INDEX } from '../../../../common/constants'; import * as i18n from './translations'; +import { + EXCLUDE_ELASTIC_CLOUD_INDEX, + getScopeFromPath, + useSourcererDataView, +} from '../../containers/sourcerer'; +import { InputsModelId } from '../../store/inputs/constants'; +import { SourcererScopeName } from '../../store/sourcerer/model'; const DescriptionListStyled = styled(EuiDescriptionList)` @media only screen and (min-width: ${(props) => props.theme.eui.euiBreakpoints.s}) { @@ -40,12 +48,12 @@ const DescriptionListStyled = styled(EuiDescriptionList)` DescriptionListStyled.displayName = 'DescriptionListStyled'; interface ModalInspectProps { - closeModal: () => void; - isShowing: boolean; - request: string | null; - response: string | null; additionalRequests?: string[] | null; additionalResponses?: string[] | null; + closeModal: () => void; + inputId?: InputsModelId; + request: string; + response: string; title: string | React.ReactElement | React.ReactNode; } @@ -101,18 +109,18 @@ export const formatIndexPatternRequested = (indices: string[] = []) => { }; export const ModalInspectQuery = ({ + additionalRequests, + additionalResponses, closeModal, - isShowing = false, + inputId, request, response, - additionalRequests, - additionalResponses, title, }: ModalInspectProps) => { - if (!isShowing || request == null || response == null) { - return null; - } - + const { pathname } = useLocation(); + const { selectedPatterns } = useSourcererDataView( + inputId === 'timeline' ? SourcererScopeName.timeline : getScopeFromPath(pathname) + ); const requests: string[] = [request, ...(additionalRequests != null ? additionalRequests : [])]; const responses: string[] = [ response, @@ -122,6 +130,16 @@ export const ModalInspectQuery = ({ const inspectRequests: Request[] = parseInspectStrings(requests); const inspectResponses: Response[] = parseInspectStrings(responses); + const isSourcererPattern = useMemo( + () => (inspectRequests[0]?.index ?? []).every((pattern) => selectedPatterns.includes(pattern)), + [inspectRequests, selectedPatterns] + ); + + const isLogsExclude = useMemo( + () => (inspectRequests[0]?.index ?? []).includes(EXCLUDE_ELASTIC_CLOUD_INDEX), + [inspectRequests] + ); + const statistics: Array<{ title: NonNullable; description: NonNullable; @@ -135,7 +153,22 @@ export const ModalInspectQuery = ({ ), description: ( - {formatIndexPatternRequested(inspectRequests[0]?.index ?? [])} +

{formatIndexPatternRequested(inspectRequests[0]?.index ?? [])}

+ + {!isSourcererPattern && ( +

+ + {i18n.INSPECT_PATTERN_DIFFERENT} + +

+ )} + {isLogsExclude && ( +

+ + {i18n.LOGS_EXCLUDE_MESSAGE} + +

+ )}
), }, diff --git a/x-pack/plugins/security_solution/public/common/components/inspect/translations.ts b/x-pack/plugins/security_solution/public/common/components/inspect/translations.ts index 28561aadf8d7e..732432c659d4c 100644 --- a/x-pack/plugins/security_solution/public/common/components/inspect/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/inspect/translations.ts @@ -36,6 +36,21 @@ export const INDEX_PATTERN_DESC = i18n.translate( } ); +export const INSPECT_PATTERN_DIFFERENT = i18n.translate( + 'xpack.securitySolution.inspectPatternDifferent', + { + defaultMessage: 'This element has a unique index pattern separate from the data view setting.', + } +); + +export const LOGS_EXCLUDE_MESSAGE = i18n.translate( + 'xpack.securitySolution.inspectPatternExcludeLogs', + { + defaultMessage: + 'When the logs-* index pattern is selected, Elastic cloud logs are excluded from the search.', + } +); + export const QUERY_TIME = i18n.translate('xpack.securitySolution.inspect.modal.queryTimeLabel', { defaultMessage: 'Query time', });