diff --git a/src/plugins/discover/public/hooks/use_es_doc_search.test.tsx b/src/plugins/discover/public/hooks/use_es_doc_search.test.tsx index a059ae721ecad..7a482ab086501 100644 --- a/src/plugins/discover/public/hooks/use_es_doc_search.test.tsx +++ b/src/plugins/discover/public/hooks/use_es_doc_search.test.tsx @@ -6,9 +6,9 @@ * Side Public License, v 1. */ -import { renderHook } from '@testing-library/react-hooks'; +import { renderHook, act } from '@testing-library/react-hooks'; import { buildSearchBody, useEsDocSearch } from './use_es_doc_search'; -import { Observable } from 'rxjs'; +import { Subject } from 'rxjs'; import { DataView } from '@kbn/data-views-plugin/public'; import { DocProps } from '../application/doc/components/doc'; import { ElasticRequestState } from '../application/doc/types'; @@ -16,8 +16,7 @@ import { SEARCH_FIELDS_FROM_SOURCE as mockSearchFieldsFromSource } from '../../c import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import React from 'react'; -const mockSearchResult = new Observable(); - +const mockSearchResult = new Subject(); const services = { data: { search: { @@ -171,23 +170,69 @@ describe('Test of helper / hook', () => { `); }); - test('useEsDocSearch', async () => { + test('useEsDocSearch loading', async () => { + const indexPattern = { + getComputedFields: () => [], + }; + const props = { + id: '1', + index: 'index1', + indexPattern, + } as unknown as DocProps; + + const hook = renderHook((p: DocProps) => useEsDocSearch(p), { + initialProps: props, + wrapper: ({ children }) => ( + {children} + ), + }); + + expect(hook.result.current.slice(0, 2)).toEqual([ElasticRequestState.Loading, null]); + }); + + test('useEsDocSearch ignore partial results', async () => { const indexPattern = { getComputedFields: () => [], }; + + const record = { test: 1 }; + const props = { id: '1', index: 'index1', indexPattern, } as unknown as DocProps; - const { result } = renderHook((p: DocProps) => useEsDocSearch(p), { + const hook = renderHook((p: DocProps) => useEsDocSearch(p), { initialProps: props, wrapper: ({ children }) => ( {children} ), }); - expect(result.current.slice(0, 2)).toEqual([ElasticRequestState.Loading, null]); + await act(async () => { + mockSearchResult.next({ + isPartial: true, + isRunning: false, + rawResponse: { + hits: { + hits: [], + }, + }, + }); + mockSearchResult.next({ + isPartial: false, + isRunning: false, + rawResponse: { + hits: { + hits: [record], + }, + }, + }); + mockSearchResult.complete(); + await hook.waitForNextUpdate(); + }); + + expect(hook.result.current.slice(0, 2)).toEqual([ElasticRequestState.Found, record]); }); }); diff --git a/src/plugins/discover/public/hooks/use_es_doc_search.ts b/src/plugins/discover/public/hooks/use_es_doc_search.ts index 27393dca72da3..84e759962de04 100644 --- a/src/plugins/discover/public/hooks/use_es_doc_search.ts +++ b/src/plugins/discover/public/hooks/use_es_doc_search.ts @@ -8,7 +8,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { firstValueFrom } from 'rxjs'; +import { lastValueFrom } from 'rxjs'; import { DataView } from '@kbn/data-views-plugin/public'; import { DocProps } from '../application/doc/components/doc'; import { ElasticRequestState } from '../application/doc/types'; @@ -18,47 +18,6 @@ import { useDiscoverServices } from './use_discover_services'; type RequestBody = Pick; -/** - * helper function to build a query body for Elasticsearch - * https://www.elastic.co/guide/en/elasticsearch/reference/current//query-dsl-ids-query.html - */ -export function buildSearchBody( - id: string, - indexPattern: DataView, - useNewFieldsApi: boolean, - requestAllFields?: boolean -): RequestBody | undefined { - const computedFields = indexPattern.getComputedFields(); - const runtimeFields = computedFields.runtimeFields as estypes.MappingRuntimeFields; - const request: RequestBody = { - body: { - query: { - ids: { - values: [id], - }, - }, - stored_fields: computedFields.storedFields, - script_fields: computedFields.scriptFields, - version: true, - }, - }; - if (!request.body) { - return undefined; - } - if (useNewFieldsApi) { - // @ts-expect-error - request.body.fields = [{ field: '*', include_unmapped: 'true' }]; - request.body.runtime_mappings = runtimeFields ? runtimeFields : {}; - if (requestAllFields) { - request.body._source = true; - } - } else { - request.body._source = true; - } - request.body.fields = [...(request.body?.fields || []), ...(computedFields.docvalueFields || [])]; - return request; -} - /** * Custom react hook for querying a single doc in ElasticSearch */ @@ -75,7 +34,7 @@ export function useEsDocSearch({ const requestData = useCallback(async () => { try { - const { rawResponse } = await firstValueFrom( + const result = await lastValueFrom( data.search.search({ params: { index, @@ -83,6 +42,7 @@ export function useEsDocSearch({ }, }) ); + const rawResponse = result.rawResponse; const hits = rawResponse.hits; @@ -109,3 +69,44 @@ export function useEsDocSearch({ return [status, hit, requestData]; } + +/** + * helper function to build a query body for Elasticsearch + * https://www.elastic.co/guide/en/elasticsearch/reference/current//query-dsl-ids-query.html + */ +export function buildSearchBody( + id: string, + indexPattern: DataView, + useNewFieldsApi: boolean, + requestAllFields?: boolean +): RequestBody | undefined { + const computedFields = indexPattern.getComputedFields(); + const runtimeFields = computedFields.runtimeFields as estypes.MappingRuntimeFields; + const request: RequestBody = { + body: { + query: { + ids: { + values: [id], + }, + }, + stored_fields: computedFields.storedFields, + script_fields: computedFields.scriptFields, + version: true, + }, + }; + if (!request.body) { + return undefined; + } + if (useNewFieldsApi) { + // @ts-expect-error + request.body.fields = [{ field: '*', include_unmapped: 'true' }]; + request.body.runtime_mappings = runtimeFields ? runtimeFields : {}; + if (requestAllFields) { + request.body._source = true; + } + } else { + request.body._source = true; + } + request.body.fields = [...(request.body?.fields || []), ...(computedFields.docvalueFields || [])]; + return request; +} diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.test.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.test.tsx index 7e38fcf81c678..e361c4d98bb77 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.test.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import 'brace'; -import { of } from 'rxjs'; +import { of, Subject } from 'rxjs'; import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; import { act } from 'react-dom/test-utils'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; @@ -220,6 +220,42 @@ describe('EsQueryAlertTypeExpression', () => { ); }); + test('should show success message if Test Query is successful (with partial result)', async () => { + const partial = { + isRunning: true, + isPartial: true, + }; + const complete = { + isRunning: false, + isPartial: false, + rawResponse: { + hits: { + total: 1234, + }, + }, + }; + const searchResponseMock$ = new Subject(); + dataMock.search.search.mockImplementation(() => searchResponseMock$); + const wrapper = await setup(defaultEsQueryExpressionParams); + const testQueryButton = wrapper.find('EuiButton[data-test-subj="testQuery"]'); + + testQueryButton.simulate('click'); + expect(dataMock.search.search).toHaveBeenCalled(); + await act(async () => { + searchResponseMock$.next(partial); + searchResponseMock$.next(complete); + searchResponseMock$.complete(); + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="testQuerySuccess"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="testQueryError"]').exists()).toBeFalsy(); + expect(wrapper.find('EuiText[data-test-subj="testQuerySuccess"]').text()).toEqual( + `Query matched 1234 documents in the last 15s.` + ); + }); + test('should show error message if Test Query is throws error', async () => { dataMock.search.search.mockImplementation(() => { throw new Error('What is this query'); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.tsx index 92096ba4541c4..97bf42ca2599a 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/es_query_expression.tsx @@ -6,7 +6,7 @@ */ import React, { useState, Fragment, useEffect, useCallback } from 'react'; -import { firstValueFrom } from 'rxjs'; +import { lastValueFrom } from 'rxjs'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -141,7 +141,7 @@ export const EsQueryExpression = ({ const timeWindow = parseDuration(window); const parsedQuery = JSON.parse(esQuery); const now = Date.now(); - const { rawResponse } = await firstValueFrom( + const { rawResponse } = await lastValueFrom( data.search.search({ params: buildSortedEventsQuery({ index, diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.test.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.test.tsx index 091fd606e1bf0..1ad76de08f5e7 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.test.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression.test.tsx @@ -14,8 +14,8 @@ import { EsQueryAlertParams, SearchType } from '../types'; import { SearchSourceExpression } from './search_source_expression'; import { chartPluginMock } from '@kbn/charts-plugin/public/mocks'; import { act } from 'react-dom/test-utils'; -import { of } from 'rxjs'; -import { IKibanaSearchResponse, ISearchSource } from '@kbn/data-plugin/common'; +import { Subject } from 'rxjs'; +import { ISearchSource } from '@kbn/data-plugin/common'; import { IUiSettingsClient } from '@kbn/core/public'; import { findTestSubject } from '@elastic/eui/lib/test'; import { EuiLoadingSpinner } from '@elastic/eui'; @@ -40,6 +40,20 @@ const defaultSearchSourceExpressionParams: EsQueryAlertParams { - return of({ - rawResponse: { - hits: { - total: 1234, - }, - }, - }); + return mockSearchResult; }), } as unknown as ISearchSource; @@ -143,6 +151,7 @@ describe('SearchSourceAlertTypeExpression', () => { wrapper = await wrapper.update(); expect(findTestSubject(wrapper, 'thresholdExpression')).toBeTruthy(); }); + test('should show success message if Test Query is successful', async () => { let wrapper = setup(defaultSearchSourceExpressionParams); await act(async () => { @@ -156,6 +165,9 @@ describe('SearchSourceAlertTypeExpression', () => { wrapper = await wrapper.update(); await act(async () => { + mockSearchResult.next(testResultPartial); + mockSearchResult.next(testResultComplete); + mockSearchResult.complete(); await nextTick(); wrapper.update(); }); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx index c351a1fe04c6a..bd03babf85a0b 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx @@ -7,7 +7,7 @@ import React, { Fragment, useCallback, useEffect, useMemo, useReducer, useState } from 'react'; import deepEqual from 'fast-deep-equal'; -import { firstValueFrom } from 'rxjs'; +import { lastValueFrom } from 'rxjs'; import { Filter } from '@kbn/es-query'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiSpacer, EuiTitle } from '@elastic/eui'; @@ -183,7 +183,7 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp 'filter', timeFilter ? [timeFilter, ...ruleConfiguration.filter] : ruleConfiguration.filter ); - const { rawResponse } = await firstValueFrom(testSearchSource.fetch$()); + const { rawResponse } = await lastValueFrom(testSearchSource.fetch$()); return { nrOfDocs: totalHitsToNumber(rawResponse.hits.total), timeWindow }; }, [searchSource, timeWindowSize, timeWindowUnit, ruleConfiguration]);