diff --git a/changelogs/fragments/8212.yml b/changelogs/fragments/8212.yml new file mode 100644 index 000000000000..5a226348dc76 --- /dev/null +++ b/changelogs/fragments/8212.yml @@ -0,0 +1,2 @@ +feat: +- Add loading indicator and counter to query result ([#8212](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8212)) \ No newline at end of file diff --git a/src/plugins/data/public/query/query_string/language_service/lib/__snapshots__/query_result.test.tsx.snap b/src/plugins/data/public/query/query_string/language_service/lib/__snapshots__/query_result.test.tsx.snap new file mode 100644 index 000000000000..cec14cdb7fa2 --- /dev/null +++ b/src/plugins/data/public/query/query_string/language_service/lib/__snapshots__/query_result.test.tsx.snap @@ -0,0 +1,62 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Query Result show error status with error message 2`] = ` + + + Error + + + } + closePopover={[Function]} + data-test-subj="queryResultError" + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="s" +> + + ERRORS + +
+ + + Reasons: + + error reason + + +

+ + Details: + + error details +

+
+
+
+`; + +exports[`Query Result shows loading status 1`] = `""`; diff --git a/src/plugins/data/public/query/query_string/language_service/lib/query_result.test.tsx b/src/plugins/data/public/query/query_string/language_service/lib/query_result.test.tsx new file mode 100644 index 000000000000..9e735cd02d64 --- /dev/null +++ b/src/plugins/data/public/query/query_string/language_service/lib/query_result.test.tsx @@ -0,0 +1,109 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; +import { QueryResult } from './query_result'; + +enum ResultStatus { + UNINITIALIZED = 'uninitialized', + LOADING = 'loading', // initial data load + READY = 'ready', // results came back + NO_RESULTS = 'none', // no results came back + ERROR = 'error', // error occurred +} + +describe('Query Result', () => { + it('shows loading status', () => { + const props = { + queryStatus: { + status: ResultStatus.LOADING, + startTime: Number.NEGATIVE_INFINITY, + }, + }; + const component = shallowWithIntl(); + expect(component).toMatchSnapshot(); + }); + + it('shows ready status with complete message', () => { + const props = { + queryStatus: { + status: ResultStatus.READY, + startTime: new Date().getTime(), + }, + }; + const component = mountWithIntl(); + const loadingIndicator = component.find(`[data-test-subj="queryResultLoading"]`); + expect(loadingIndicator.exists()).toBeFalsy(); + expect(component.find('EuiText').text()).toEqual('Completed'); + }); + + it('shows ready status with complete in miliseconds message', () => { + const props = { + queryStatus: { + status: ResultStatus.READY, + startTime: new Date().getTime(), + elapsedMs: 500, + }, + }; + const component = mountWithIntl(); + expect(component.find('EuiText').text()).toEqual('Completed in 500 ms'); + }); + + it('shows ready status with complete in seconds message', () => { + const props = { + queryStatus: { + status: ResultStatus.READY, + startTime: new Date().getTime(), + elapsedMs: 2000, + }, + }; + const component = mountWithIntl(); + expect(component.find('EuiText').text()).toEqual('Completed in 2.0 s'); + }); + + it('shows ready status with split seconds', () => { + const props = { + queryStatus: { + status: ResultStatus.READY, + startTime: new Date().getTime(), + elapsedMs: 2700, + }, + }; + const component = mountWithIntl(); + expect(component.find('EuiText').text()).toEqual('Completed in 2.7 s'); + }); + + it('show error status with error message', () => { + const props = { + queryStatus: { + status: ResultStatus.ERROR, + body: { + error: { + reason: 'error reason', + details: 'error details', + }, + statusCode: 400, + }, + }, + }; + const component = shallowWithIntl(); + expect(component.find(`[data-test-subj="queryResultError"]`).text()).toMatchInlineSnapshot( + `""` + ); + component.find(`[data-test-subj="queryResultError"]`).simulate('click'); + expect(component).toMatchSnapshot(); + }); + + it('returns null when error body is empty', () => { + const props = { + queryStatus: { + status: ResultStatus.ERROR, + }, + }; + const component = shallowWithIntl(); + expect(component).toEqual({}); + }); +}); diff --git a/src/plugins/data/public/query/query_string/language_service/lib/query_result.tsx b/src/plugins/data/public/query/query_string/language_service/lib/query_result.tsx index 9806b7cd55af..3a8adb0675ca 100644 --- a/src/plugins/data/public/query/query_string/language_service/lib/query_result.tsx +++ b/src/plugins/data/public/query/query_string/language_service/lib/query_result.tsx @@ -8,7 +8,7 @@ import { i18n } from '@osd/i18n'; import './_recent_query.scss'; import { EuiButtonEmpty, EuiPopover, EuiText, EuiPopoverTitle } from '@elastic/eui'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; export enum ResultStatus { UNINITIALIZED = 'uninitialized', @@ -28,21 +28,77 @@ export interface QueryStatus { statusCode?: number; }; elapsedMs?: number; + startTime?: number; } +// This is the time in milliseconds that the query will wait before showing the loading spinner +const BUFFER_TIME = 3000; + export function QueryResult(props: { queryStatus: QueryStatus }) { const [isPopoverOpen, setPopover] = useState(false); + const [elapsedTime, setElapsedTime] = useState(0); const onButtonClick = () => { setPopover(!isPopoverOpen); }; + const updateElapsedTime = () => { + const time = Date.now() - (props.queryStatus.startTime || 0); + if (time > BUFFER_TIME) { + setElapsedTime(time); + } else { + setElapsedTime(0); + } + }; + + useEffect(() => { + const interval = setInterval(updateElapsedTime, 1000); + + return () => clearInterval(interval); + }); + + if (props.queryStatus.status === ResultStatus.LOADING) { + if (elapsedTime < BUFFER_TIME) { + return null; + } + const time = Math.floor(elapsedTime / 1000); + return ( + {}} + isLoading + data-test-subj="queryResultLoading" + > + {i18n.translate('data.query.languageService.queryResults.completeTime', { + defaultMessage: `Loading ${time} s`, + })} + + ); + } + if (props.queryStatus.status === ResultStatus.READY) { + let message; + if (!props.queryStatus.elapsedMs) { + message = i18n.translate('data.query.languageService.queryResults.completeTime', { + defaultMessage: `Completed`, + }); + } else if (props.queryStatus.elapsedMs < 1000) { + message = i18n.translate( + 'data.query.languageService.queryResults.completeTimeInMiliseconds', + { + defaultMessage: `Completed in ${props.queryStatus.elapsedMs} ms`, + } + ); + } else { + message = i18n.translate('data.query.languageService.queryResults.completeTimeInSeconds', { + defaultMessage: `Completed in ${(props.queryStatus.elapsedMs / 1000).toFixed(1)} s`, + }); + } + return ( {}}> - - {props.queryStatus.elapsedMs - ? `Completed in ${props.queryStatus.elapsedMs} ms` - : 'Completed'} + + {message} ); @@ -55,9 +111,17 @@ export function QueryResult(props: { queryStatus: QueryStatus }) { return ( + - {'Error'} + {i18n.translate('data.query.languageService.queryResults.error', { + defaultMessage: `Error`, + })} } @@ -65,6 +129,7 @@ export function QueryResult(props: { queryStatus: QueryStatus }) { closePopover={() => setPopover(false)} panelPaddingSize="s" anchorPosition={'downRight'} + data-test-subj="queryResultError" > ERRORS
@@ -82,7 +147,7 @@ export function QueryResult(props: { queryStatus: QueryStatus }) { {i18n.translate('data.query.languageService.queryResults.details', { defaultMessage: `Details:`, })} - {' '} + {props.queryStatus.body.error.details}

diff --git a/src/plugins/discover/public/application/view_components/utils/use_search.ts b/src/plugins/discover/public/application/view_components/utils/use_search.ts index 91b896f143f1..88300cc570fa 100644 --- a/src/plugins/discover/public/application/view_components/utils/use_search.ts +++ b/src/plugins/discover/public/application/view_components/utils/use_search.ts @@ -60,6 +60,7 @@ export interface SearchData { statusCode?: number; }; elapsedMs?: number; + startTime?: number; }; } @@ -119,12 +120,14 @@ export const useSearch = (services: DiscoverViewServices) => { ); }, [savedSearch, services.uiSettings, timefilter]); + const startTime = Date.now(); const data$ = useMemo( () => new BehaviorSubject({ status: shouldSearchOnPageLoad() ? ResultStatus.LOADING : ResultStatus.UNINITIALIZED, + queryStatus: { startTime }, }), - [shouldSearchOnPageLoad] + [shouldSearchOnPageLoad, startTime] ); const refetch$ = useMemo(() => new Subject(), []); @@ -161,7 +164,6 @@ export const useSearch = (services: DiscoverViewServices) => { dataset = searchSource.getField('index'); let elapsedMs; - try { // Only show loading indicator if we are fetching when the rows are empty if (fetchStateRef.current.rows?.length === 0) { @@ -267,14 +269,14 @@ export const useSearch = (services: DiscoverViewServices) => { } }, [ indexPattern, - interval, timefilter, toastNotifications, + interval, data, services, + sort, savedSearch?.searchSource, data$, - sort, shouldSearchOnPageLoad, inspectorAdapters.requests, ]);