Skip to content

Commit

Permalink
Improve query assist user experiences (opensearch-project#1817)
Browse files Browse the repository at this point in the history
Signed-off-by: Joshua Li <[email protected]>
  • Loading branch information
joshuali925 committed May 20, 2024
1 parent bf0d141 commit d1ed5b5
Show file tree
Hide file tree
Showing 24 changed files with 481 additions and 66 deletions.
69 changes: 66 additions & 3 deletions public/components/common/search/__tests__/search.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { applyMiddleware, createStore } from '@reduxjs/toolkit';
import { render } from '@testing-library/react';
import { configure, mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import React from 'react';
import { applyMiddleware, createStore } from '@reduxjs/toolkit';
import { rootReducer } from '../../../../framework/redux/reducers';
import { Provider } from 'react-redux';
import { Search } from '../search';
import thunk from 'redux-thunk';
import { coreRefs } from '../../../../framework/core_refs';
import { rootReducer } from '../../../../framework/redux/reducers';
import { initialTabId } from '../../../../framework/redux/store/shared_state';
import * as hookExports from '../../../event_analytics/explorer/query_assist/hooks';
import { Search } from '../search';

describe('Explorer Search component', () => {
configure({ adapter: new Adapter() });
Expand All @@ -27,3 +30,63 @@ describe('Explorer Search component', () => {
expect(wrapper).toMatchSnapshot();
});
});

describe('Search state', () => {
const catIndicesSpy = jest.spyOn(hookExports, 'useCatIndices');
const getIndexPatternsSpy = jest.spyOn(hookExports, 'useGetIndexPatterns');
const store = createStore(rootReducer, applyMiddleware(thunk));

beforeEach(() => {
coreRefs.queryAssistEnabled = true;
});

afterEach(() => {
jest.clearAllMocks();
});

it('selects logs sample data over others', async () => {
catIndicesSpy.mockReturnValue({
data: [{ label: 'opensearch_dashboards_sample_data_flights' }],
loading: false,
refresh: jest.fn(),
});
getIndexPatternsSpy.mockReturnValue({
data: [{ label: 'test-index' }, { label: 'opensearch_dashboards_sample_data_logs' }],
loading: false,
refresh: jest.fn(),
});
const component = render(
<Provider store={store}>
<Search
tabId={initialTabId}
handleQueryChange={jest.fn()}
pplService={{ fetch: () => Promise.resolve() }}
/>
</Provider>
);
expect(component.getByText('opensearch_dashboards_sample_data_logs')).toBeInTheDocument();
});

it('selects other sample data', async () => {
catIndicesSpy.mockReturnValue({
data: [{ label: 'test-index' }, { label: 'opensearch_dashboards_sample_data_flights' }],
loading: false,
refresh: jest.fn(),
});
getIndexPatternsSpy.mockReturnValue({
data: [],
loading: false,
refresh: jest.fn(),
});
const component = render(
<Provider store={store}>
<Search
tabId={initialTabId}
handleQueryChange={jest.fn()}
pplService={{ fetch: () => Promise.resolve() }}
/>
</Provider>
);
expect(component.getByText('opensearch_dashboards_sample_data_flights')).toBeInTheDocument();
});
});
2 changes: 1 addition & 1 deletion public/components/common/search/query_area.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export function QueryArea({
const memoizedGetAvailableFields = useMemo(() => getAvailableFields, []);
const memoizedHandleQueryChange = useMemo(() => handleQueryChange, []);
useEffect(() => {
const indexQuery = `source = ${selectedIndex[0].label}`;
const indexQuery = `source = ${selectedIndex[0]?.label || ''}`;
memoizedHandleQueryChange(indexQuery);
memoizedGetAvailableFields(indexQuery);
}, [selectedIndex, memoizedGetAvailableFields, memoizedHandleQueryChange]);
Expand Down
36 changes: 29 additions & 7 deletions public/components/common/search/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ import {
OLLY_QUERY_ASSISTANT,
RAW_QUERY,
} from '../../../../common/constants/explorer';
import { PPL_SPAN_REGEX } from '../../../../common/constants/shared';
import {
PPL_SPAN_REGEX,
QUERY_ASSIST_END_TIME,
QUERY_ASSIST_START_TIME,
} from '../../../../common/constants/shared';
import { uiSettingsService } from '../../../../common/utils';
import { useFetchEvents } from '../../../components/event_analytics/hooks';
import { usePolling } from '../../../components/hooks/use_polling';
Expand Down Expand Up @@ -277,24 +281,42 @@ export const Search = (props: any) => {
dispatch(changeQuery({ tabId, query: { [RAW_QUERY]: tempQuery } }));
});
onQuerySearch(queryLang);
handleTimePickerChange([startTime, endTime]);
if (coreRefs.queryAssistEnabled) {
handleTimePickerChange([QUERY_ASSIST_START_TIME, QUERY_ASSIST_END_TIME]);
} else {
handleTimePickerChange([startTime, endTime]);
}
setNeedsUpdate(false);
};

// STATE FOR LANG PICKER AND INDEX PICKER
const [selectedIndex, setSelectedIndex] = useState<EuiComboBoxOptionOption[]>([
{ label: 'opensearch_dashboards_sample_data_logs' },
]);
const [selectedIndex, setSelectedIndex] = useState<EuiComboBoxOptionOption[]>([]);
const { data: indices, loading: indicesLoading } = useCatIndices();
const { data: indexPatterns, loading: indexPatternsLoading } = useGetIndexPatterns();
const indicesAndIndexPatterns =
indexPatterns && indices
? [...indexPatterns, ...indices].filter(
(v1, index, array) => array.findIndex((v2) => v1.label === v2.label) === index
)
: undefined;
: [];
const loading = indicesLoading || indexPatternsLoading;

useEffect(() => {
if (selectedIndex.length || !indicesAndIndexPatterns.length) return;
// pre-fill selected index with sample logs or other sample data index
const sampleLogOption = indicesAndIndexPatterns.find(
(option) => option.label === 'opensearch_dashboards_sample_data_logs'
);
if (sampleLogOption) {
setSelectedIndex([sampleLogOption]);
return;
}
const sampleDataOption = indicesAndIndexPatterns.find((option) =>
option.label.startsWith('opensearch_dashboards_sample_data_')
);
if (sampleDataOption) setSelectedIndex([sampleDataOption]);
}, [indicesAndIndexPatterns]);

const onLanguagePopoverClick = () => {
setLanguagePopoverOpen(!_isLanguagePopoverOpen);
};
Expand Down Expand Up @@ -352,7 +374,7 @@ export const Search = (props: any) => {
<EuiFlexItem>
<EuiComboBox
placeholder="Select an index"
isClearable={true}
isClearable={false}
prepend={<EuiText>Index</EuiText>}
singleSelection={{ asPlainText: true }}
isLoading={loading}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import PPLService from '../../../../services/requests/ppl';
import { useFetchEvents } from '../../hooks';
import { redoQuery } from '../../utils/utils';
import { FlyoutButton } from './docViewRow';
import { HitsCounter } from '../hits_counter/hits_counter';
import { HitsCounter } from '../timechart/hits_counter';

export interface DataGridProps {
http: HttpSetup;
Expand Down
50 changes: 25 additions & 25 deletions public/components/event_analytics/explorer/explorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ import {
EuiText,
} from '@elastic/eui';
import { FormattedMessage } from '@osd/i18n/react';
import _, { isEmpty, isEqual, reduce } from 'lodash';
import { createBrowserHistory } from 'history';
import isEmpty from 'lodash/isEmpty';
import isEqual from 'lodash/isEqual';
import reduce from 'lodash/reduce';
import sum from 'lodash/sum';
import React, {
ReactElement,
useCallback,
Expand All @@ -32,7 +36,6 @@ import React, {
useState,
} from 'react';
import { batch, useDispatch, useSelector } from 'react-redux';
import { createBrowserHistory } from 'history';
import { LogExplorerRouterContext } from '..';
import {
DEFAULT_DATA_SOURCE_TYPE,
Expand Down Expand Up @@ -127,13 +130,11 @@ import { formatError, getContentTabTitle } from '../utils/utils';
import { DataSourceSelection } from './datasources/datasources_selection';
import { DirectQueryRunning } from './direct_query_running';
import { DataGrid } from './events_views/data_grid';
import { HitsCounter } from './hits_counter/hits_counter';
import { LogPatterns } from './log_patterns/log_patterns';
import { NoResults } from './no_results';
import { ObservabilitySideBar } from './sidebar/observability_sidebar';
import { TimechartHeader } from './timechart_header';
import { getTimeRangeFromCountDistribution, HitsCounter, Timechart } from './timechart';
import { ExplorerVisualizations } from './visualizations';
import { CountDistribution } from './visualizations/count_distribution';
import { DirectQueryVisualization } from './visualizations/direct_query_vis';

export const Explorer = ({
Expand Down Expand Up @@ -527,12 +528,25 @@ export const Explorer = ({
handleQuerySearch();
};

/**
* If query assist is enabled, the time range is fixed to
* QUERY_ASSIST_START_TIME and QUERY_ASSIST_END_TIME and not useful. Return
* the time range based on aggregation buckets instead.
*
* @returns startTime and endTime
*/
const getTimeChartRange = (): { startTime?: string; endTime?: string } => {
if (!coreRefs.queryAssistEnabled) return { startTime, endTime };
const { startTime: start, endTime: end } = getTimeRangeFromCountDistribution(countDistribution);
return { startTime: start ?? startTime, endTime: end ?? endTime };
};

const totalHits: number = useMemo(() => {
if (isLiveTailOn && countDistribution?.data) {
const hits = reduce(
countDistribution.data['count()'],
(sum, n) => {
return sum + n;
(total, n) => {
return total + n;
},
liveHits
);
Expand All @@ -558,13 +572,9 @@ export const Explorer = ({
<EuiPanel hasBorder={false} hasShadow={false} paddingSize="s" color="transparent">
{countDistribution?.data && !isLiveTailOnRef.current && (
<EuiPanel>
<HitsCounter
hits={_.sum(countDistribution.data?.['count()'])}
showResetButton={false}
onResetQuery={() => {}}
/>
<TimechartHeader
options={timeIntervalOptions}
<Timechart
countDistribution={countDistribution}
timeIntervalOptions={timeIntervalOptions}
onChangeInterval={(selectedIntrv) => {
const intervalOptionsIndex = timeIntervalOptions.findIndex(
(item) => item.value === selectedIntrv
Expand All @@ -580,20 +590,10 @@ export const Explorer = ({
selectedIntervalRef.current = timeIntervalOptions[intervalOptionsIndex];
getPatterns(intrv, getErrorHandler('Error fetching patterns'));
}}
stateInterval={
countDistribution.selectedInterval || selectedIntervalRef.current?.value
}
startTime={startTime}
endTime={endTime}
/>
<EuiSpacer size="s" />
<CountDistribution
countDistribution={countDistribution}
selectedInterval={
countDistribution.selectedInterval || selectedIntervalRef.current?.value
}
startTime={startTime}
endTime={endTime}
{...getTimeChartRange()}
/>
</EuiPanel>
)}
Expand Down
7 changes: 1 addition & 6 deletions public/components/event_analytics/explorer/log_explorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,8 @@ import { EmptyTabParams, ILogExplorerProps } from '../../../../common/types/expl
import { selectQueryResult } from '../redux/slices/query_result_slice';
import { selectQueries } from '../redux/slices/query_slice';
import { selectQueryTabs } from '../redux/slices/query_tab_slice';
import { Explorer } from './explorer';
import { getDateRange } from '../utils/utils';
import {
QUERY_ASSIST_END_TIME,
QUERY_ASSIST_START_TIME,
} from '../../../../common/constants/shared';
import { coreRefs } from '../../../../public/framework/core_refs';
import { Explorer } from './explorer';

const searchBarConfigs = {
[TAB_EVENT_ID]: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ exports[`Callouts spec EmptyQueryCallOut should match snapshot 1`] = `
<div>
<div
class="euiCallOut euiCallOut--warning euiCallOut--small"
data-test-subj="query-assist-empty-callout"
data-test-subj="query-assist-empty-query-callout"
>
<div
class="euiCallOutHeader"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,21 @@ export const ProhibitedQueryCallOut: React.FC<QueryAssistCallOutProps> = (props)
/>
);

export const EmptyIndexCallOut: React.FC<QueryAssistCallOutProps> = (props) => (
<EuiCallOut
data-test-subj="query-assist-empty-index-callout"
title="Select a data source or index to ask a question."
size="s"
color="warning"
iconType="iInCircle"
dismissible
onDismiss={props.onDismiss}
/>
);

export const EmptyQueryCallOut: React.FC<QueryAssistCallOutProps> = (props) => (
<EuiCallOut
data-test-subj="query-assist-empty-callout"
data-test-subj="query-assist-empty-query-callout"
title="Enter a natural language question to automatically generate a query to view results."
size="s"
color="warning"
Expand Down
Loading

0 comments on commit d1ed5b5

Please sign in to comment.