Skip to content

Commit

Permalink
Add loading indicator and counter to query result (#8212) (#8273)
Browse files Browse the repository at this point in the history
* add loading indicator and counter to query result



* Changeset file for PR #8212 created/updated

* add unit tests



* address comments



* update snapshot



---------



(cherry picked from commit 27ba36c)

Signed-off-by: abbyhu2000 <[email protected]>
Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Sep 20, 2024
1 parent 10bb541 commit aa42f67
Show file tree
Hide file tree
Showing 5 changed files with 252 additions and 12 deletions.
2 changes: 2 additions & 0 deletions changelogs/fragments/8212.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
feat:
- Add loading indicator and counter to query result ([#8212](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8212))

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -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(<QueryResult {...props} />);
expect(component).toMatchSnapshot();
});

it('shows ready status with complete message', () => {
const props = {
queryStatus: {
status: ResultStatus.READY,
startTime: new Date().getTime(),
},
};
const component = mountWithIntl(<QueryResult {...props} />);
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(<QueryResult {...props} />);
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(<QueryResult {...props} />);
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(<QueryResult {...props} />);
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(<QueryResult {...props} />);
expect(component.find(`[data-test-subj="queryResultError"]`).text()).toMatchInlineSnapshot(
`"<EuiPopover />"`
);
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(<QueryResult {...props} />);
expect(component).toEqual({});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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 (
<EuiButtonEmpty
color="text"
size="xs"
onClick={() => {}}
isLoading
data-test-subj="queryResultLoading"
>
{i18n.translate('data.query.languageService.queryResults.completeTime', {
defaultMessage: `Loading ${time} s`,
})}
</EuiButtonEmpty>
);
}

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 (
<EuiButtonEmpty iconSide="left" iconType={'checkInCircleEmpty'} size="xs" onClick={() => {}}>
<EuiText size="xs" color="subdued">
{props.queryStatus.elapsedMs
? `Completed in ${props.queryStatus.elapsedMs} ms`
: 'Completed'}
<EuiText size="xs" color="subdued" data-test-subj="queryResultCompleteMsg">
{message}
</EuiText>
</EuiButtonEmpty>
);
Expand All @@ -55,16 +111,25 @@ export function QueryResult(props: { queryStatus: QueryStatus }) {
return (
<EuiPopover
button={
<EuiButtonEmpty iconSide="left" iconType={'alert'} size="xs" onClick={onButtonClick}>
<EuiButtonEmpty
iconSide="left"
iconType={'alert'}
size="xs"
onClick={onButtonClick}
data-test-subj="queryResultErrorBtn"
>
<EuiText size="xs" color="subdued">
{'Error'}
{i18n.translate('data.query.languageService.queryResults.error', {
defaultMessage: `Error`,
})}
</EuiText>
</EuiButtonEmpty>
}
isOpen={isPopoverOpen}
closePopover={() => setPopover(false)}
panelPaddingSize="s"
anchorPosition={'downRight'}
data-test-subj="queryResultError"
>
<EuiPopoverTitle>ERRORS</EuiPopoverTitle>
<div style={{ width: '250px' }}>
Expand All @@ -82,7 +147,7 @@ export function QueryResult(props: { queryStatus: QueryStatus }) {
{i18n.translate('data.query.languageService.queryResults.details', {
defaultMessage: `Details:`,
})}
</strong>{' '}
</strong>
{props.queryStatus.body.error.details}
</p>
</EuiText>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export interface SearchData {
statusCode?: number;
};
elapsedMs?: number;
startTime?: number;
};
}

Expand Down Expand Up @@ -119,12 +120,14 @@ export const useSearch = (services: DiscoverViewServices) => {
);
}, [savedSearch, services.uiSettings, timefilter]);

const startTime = Date.now();
const data$ = useMemo(
() =>
new BehaviorSubject<SearchData>({
status: shouldSearchOnPageLoad() ? ResultStatus.LOADING : ResultStatus.UNINITIALIZED,
queryStatus: { startTime },
}),
[shouldSearchOnPageLoad]
[shouldSearchOnPageLoad, startTime]
);
const refetch$ = useMemo(() => new Subject<SearchRefetch>(), []);

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -267,14 +269,14 @@ export const useSearch = (services: DiscoverViewServices) => {
}
}, [
indexPattern,
interval,
timefilter,
toastNotifications,
interval,
data,
services,
sort,
savedSearch?.searchSource,
data$,
sort,
shouldSearchOnPageLoad,
inspectorAdapters.requests,
]);
Expand Down

0 comments on commit aa42f67

Please sign in to comment.