[UnifiedSearch][Lens] Make sure adHoc dataViews are receiving the right query suggestions (#143389)

* 🐛 Resolve title and id strings as dataViews

* 🐛 Make sure dataViews are passed by id when available

* 🐛 Forgot to spread results

*  Fix and add more tests

* 🐛 Fix test

*  refactor as suggested for performance

* 🏷️ reuse type

*  Fix one more test

* 🐛 Fix mock name

* Update x-pack/plugins/graph/public/components/search_bar.test.tsx

* Update x-pack/plugins/graph/public/components/search_bar.test.tsx

* 🐛 Fix instanceof issue

* 🐛 Use a mock function for dataview

Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
This commit is contained in:
Marco Liberati 2022-10-19 15:29:29 +02:00 committed by GitHub
parent e363b7c9ba
commit 6522bbb453
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 102 additions and 30 deletions

View file

@ -7,26 +7,47 @@
*/
import { isEmpty } from 'lodash';
import { DataViewsContract } from '@kbn/data-views-plugin/public';
import type { DataViewsContract, DataView } from '@kbn/data-views-plugin/public';
export interface DataViewByIdOrTitle {
type: 'title' | 'id';
value: string;
}
export async function fetchIndexPatterns(
indexPatternsService: DataViewsContract,
indexPatternStrings: string[]
) {
indexPatternStrings: DataViewByIdOrTitle[]
): Promise<DataView[]> {
if (!indexPatternStrings || isEmpty(indexPatternStrings)) {
return [];
}
const searchString = indexPatternStrings.map((string) => `"${string}"`).join(' | ');
const searchStringList: string[] = [];
const searchIdsList: string[] = [];
for (const { type, value } of indexPatternStrings) {
if (type === 'title') {
searchStringList.push(value);
} else {
searchIdsList.push(value);
}
}
const exactMatches = (await indexPatternsService.find(searchString)).filter((ip) =>
indexPatternStrings.includes(ip.title)
);
const searchString = searchStringList.map((value) => `"${value}"`).join(' | ');
const [searchMatches, ...matchesById] = await Promise.all([
indexPatternsService.find(searchString),
...searchIdsList.map((id) => indexPatternsService.get(id)),
]);
const exactMatches = [
...searchMatches.filter((ip) => searchStringList.includes(ip.title)),
...matchesById,
];
const allMatches =
exactMatches.length === indexPatternStrings.length
? exactMatches
: [...exactMatches, await indexPatternsService.getDefault()];
return allMatches;
return allMatches.filter((d: DataView | null): d is DataView => d != null);
}

View file

@ -50,7 +50,7 @@ export const FilterEditorWrapper = React.memo(function FilterEditorWrapper({
const objectPatternsFromStrings = (await fetchIndexPatterns(
data.dataViews,
stringPatterns
stringPatterns.map((value) => ({ type: 'title', value }))
)) as DataView[];
setDataviews([...objectPatterns, ...objectPatternsFromStrings]);
const [dataView] = [...objectPatterns, ...objectPatternsFromStrings];

View file

@ -361,7 +361,46 @@ describe('QueryStringInput', () => {
disableAutoFocus: true,
})
);
expect(mockFetchIndexPatterns.mock.calls[0][1]).toStrictEqual(patternStrings);
expect(mockFetchIndexPatterns.mock.calls[0][1]).toEqual(
patternStrings.map((value) => ({ type: 'title', value }))
);
});
it('Should accept index pattern ids and fetch the full object', () => {
const idStrings = [{ type: 'id', value: '1' }];
mockFetchIndexPatterns.mockClear();
mount(
wrapQueryStringInputInContext({
query: kqlQuery,
onSubmit: noop,
indexPatterns: idStrings,
disableAutoFocus: true,
})
);
expect(mockFetchIndexPatterns.mock.calls[0][1]).toEqual(idStrings);
});
it('Should accept a mix of full objects, title and ids and fetch only missing index pattern objects', () => {
const patternStrings = [
'logstash-*',
{ type: 'id', value: '1' },
{ type: 'title', value: 'my-fake-index-pattern' },
stubIndexPattern,
];
mockFetchIndexPatterns.mockClear();
mount(
wrapQueryStringInputInContext({
query: kqlQuery,
onSubmit: noop,
indexPatterns: patternStrings,
disableAutoFocus: true,
})
);
expect(mockFetchIndexPatterns.mock.calls[0][1]).toEqual([
{ type: 'title', value: 'logstash-*' },
{ type: 'id', value: '1' },
{ type: 'title', value: 'my-fake-index-pattern' },
]);
});
it('Should convert non-breaking spaces into regular spaces', () => {

View file

@ -27,11 +27,11 @@ import {
toSentenceCase,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { compact, debounce, isEmpty, isEqual, isFunction } from 'lodash';
import { compact, debounce, isEmpty, isEqual, isFunction, partition } from 'lodash';
import { CoreStart, DocLinksStart, Toast } from '@kbn/core/public';
import type { Query } from '@kbn/es-query';
import { DataPublicPluginStart, getQueryLog } from '@kbn/data-plugin/public';
import { DataView } from '@kbn/data-views-plugin/public';
import { type DataView, DataView as KibanaDataView } from '@kbn/data-views-plugin/public';
import type { PersistedLog } from '@kbn/data-plugin/public';
import { getFieldSubtypeNested, KIBANA_USER_QUERY_LANGUAGE_KEY } from '@kbn/data-plugin/common';
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
@ -40,7 +40,7 @@ import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public';
import { matchPairs } from './match_pairs';
import { toUser } from './to_user';
import { fromUser } from './from_user';
import { fetchIndexPatterns } from './fetch_index_patterns';
import { type DataViewByIdOrTitle, fetchIndexPatterns } from './fetch_index_patterns';
import { QueryLanguageSwitcher } from './language_switcher';
import type { SuggestionsListSize } from '../typeahead/suggestions_component';
import { SuggestionsComponent } from '../typeahead';
@ -64,7 +64,7 @@ export interface QueryStringInputDependencies {
}
export interface QueryStringInputProps {
indexPatterns: Array<DataView | string>;
indexPatterns: Array<DataView | string | DataViewByIdOrTitle>;
query: Query;
disableAutoFocus?: boolean;
screenTitle?: string;
@ -179,12 +179,15 @@ export default class QueryStringInputUI extends PureComponent<QueryStringInputPr
};
private fetchIndexPatterns = debounce(async () => {
const stringPatterns = this.props.indexPatterns.filter(
(indexPattern) => typeof indexPattern === 'string'
) as string[];
const objectPatterns = this.props.indexPatterns.filter(
(indexPattern) => typeof indexPattern !== 'string'
) as DataView[];
const [objectPatterns = [], stringPatterns = []] = partition<
QueryStringInputProps['indexPatterns'][number],
DataView
>(this.props.indexPatterns || [], (indexPattern): indexPattern is DataView => {
return indexPattern instanceof KibanaDataView;
});
const idOrTitlePatterns = stringPatterns.map((sp) =>
typeof sp === 'string' ? { type: 'title', value: sp } : sp
) as DataViewByIdOrTitle[];
// abort the previous fetch to avoid overriding with outdated data
// issue https://github.com/elastic/kibana/issues/80831
@ -192,10 +195,10 @@ export default class QueryStringInputUI extends PureComponent<QueryStringInputPr
this.fetchIndexPatternsAbortController = new AbortController();
const currentAbortController = this.fetchIndexPatternsAbortController;
const objectPatternsFromStrings = (await fetchIndexPatterns(
const objectPatternsFromStrings = await fetchIndexPatterns(
this.props.deps.data.indexPatterns,
stringPatterns
)) as DataView[];
idOrTitlePatterns
);
if (!currentAbortController.signal.aborted) {
this.setState({

View file

@ -18,6 +18,7 @@ import {
} from '@kbn/core/public';
import { act } from 'react-dom/test-utils';
import { QueryStringInput } from '@kbn/unified-search-plugin/public';
import { createStubDataView } from '@kbn/data-views-plugin/common/mocks';
import type { DataView } from '@kbn/data-views-plugin/public';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { I18nProvider, InjectedIntl } from '@kbn/i18n-react';
@ -90,7 +91,7 @@ describe('search_bar', () => {
isLoading: false,
indexPatternProvider: {
get: jest.fn(() =>
Promise.resolve({ fields: [], getName: () => 'Test Name' } as unknown as DataView)
Promise.resolve(createStubDataView({ spec: { fields: {}, name: 'Test Name' } }))
),
},
confirmWipeWorkspace: (callback: () => void) => {

View file

@ -94,7 +94,7 @@ describe('filter popover', () => {
expect(instance.find(QueryStringInput).props()).toEqual(
expect.objectContaining({
dataTestSubj: 'indexPattern-filters-queryStringInput',
indexPatterns: ['my-fake-index-pattern'],
indexPatterns: [{ type: 'id', value: '1' }],
isInvalid: false,
query: { language: 'kuery', query: 'bytes >= 1' },
})

View file

@ -60,7 +60,11 @@ export const FilterPopover = ({
<QueryInput
isInvalid={!isQueryValid(filter.input, indexPattern)}
value={filter.input}
indexPatternTitle={indexPattern.title}
indexPattern={
indexPattern.id
? { type: 'id', value: indexPattern.id }
: { type: 'title', value: indexPattern.title }
}
disableAutoFocus
onChange={setFilterQuery}
onSubmit={() => {

View file

@ -129,7 +129,11 @@ export function FilterQueryInput({
data-test-subj="indexPattern-filter-by-input"
>
<QueryInput
indexPatternTitle={indexPattern.title}
indexPattern={
indexPattern.id
? { type: 'id', value: indexPattern.id }
: { type: 'title', value: indexPattern.title }
}
disableAutoFocus={true}
value={queryInput}
onChange={setQueryInput}

View file

@ -17,7 +17,7 @@ import { LensAppServices } from '../../app_plugin/types';
export const QueryInput = ({
value,
onChange,
indexPatternTitle,
indexPattern,
isInvalid,
onSubmit,
disableAutoFocus,
@ -26,7 +26,7 @@ export const QueryInput = ({
}: {
value: Query;
onChange: (input: Query) => void;
indexPatternTitle: string;
indexPattern: string | { type: 'title' | 'id'; value: string };
isInvalid: boolean;
onSubmit: () => void;
disableAutoFocus?: boolean;
@ -46,7 +46,7 @@ export const QueryInput = ({
disableAutoFocus={disableAutoFocus}
isInvalid={isInvalid}
bubbleSubmitEvent={false}
indexPatterns={[indexPatternTitle]}
indexPatterns={[indexPattern]}
query={inputValue}
onChange={(newQuery) => {
if (!isEqual(newQuery, inputValue)) {