[App Search] Added a persistent query tester flyout (#101071)
This commit is contained in:
parent
58b1416f84
commit
9618fd7dfe
|
@ -17,7 +17,7 @@ import { CurationResult, AddResultFlyout } from './';
|
|||
|
||||
describe('AddResultFlyout', () => {
|
||||
const values = {
|
||||
dataLoading: false,
|
||||
searchDataLoading: false,
|
||||
searchQuery: '',
|
||||
searchResults: [],
|
||||
promotedIds: [],
|
||||
|
@ -48,7 +48,7 @@ describe('AddResultFlyout', () => {
|
|||
|
||||
describe('search input', () => {
|
||||
it('renders isLoading state correctly', () => {
|
||||
setMockValues({ ...values, dataLoading: true });
|
||||
setMockValues({ ...values, searchDataLoading: true });
|
||||
const wrapper = shallow(<AddResultFlyout />);
|
||||
|
||||
expect(wrapper.find(EuiFieldSearch).prop('isLoading')).toEqual(true);
|
||||
|
|
|
@ -24,6 +24,7 @@ import { i18n } from '@kbn/i18n';
|
|||
|
||||
import { FlashMessages } from '../../../../../shared/flash_messages';
|
||||
|
||||
import { SearchLogic } from '../../../search';
|
||||
import {
|
||||
RESULT_ACTIONS_DIRECTIONS,
|
||||
PROMOTE_DOCUMENT_ACTION,
|
||||
|
@ -36,8 +37,10 @@ import { CurationLogic } from '../curation_logic';
|
|||
import { AddResultLogic, CurationResult } from './';
|
||||
|
||||
export const AddResultFlyout: React.FC = () => {
|
||||
const { searchQuery, searchResults, dataLoading } = useValues(AddResultLogic);
|
||||
const { search, closeFlyout } = useActions(AddResultLogic);
|
||||
const searchLogic = SearchLogic({ id: 'add-results-flyout' });
|
||||
const { searchQuery, searchResults, searchDataLoading } = useValues(searchLogic);
|
||||
const { closeFlyout } = useActions(AddResultLogic);
|
||||
const { search } = useActions(searchLogic);
|
||||
|
||||
const { promotedIds, hiddenIds } = useValues(CurationLogic);
|
||||
const { addPromotedId, removePromotedId, addHiddenId, removeHiddenId } = useActions(
|
||||
|
@ -63,7 +66,7 @@ export const AddResultFlyout: React.FC = () => {
|
|||
<EuiFieldSearch
|
||||
value={searchQuery}
|
||||
onChange={(e) => search(e.target.value)}
|
||||
isLoading={dataLoading}
|
||||
isLoading={searchDataLoading}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.curations.addResult.searchPlaceholder',
|
||||
{ defaultMessage: 'Search engine documents' }
|
||||
|
|
|
@ -5,31 +5,16 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { LogicMounter, mockHttpValues, mockFlashMessageHelpers } from '../../../../../__mocks__';
|
||||
import { LogicMounter } from '../../../../../__mocks__';
|
||||
import '../../../../__mocks__/engine_logic.mock';
|
||||
|
||||
import { nextTick } from '@kbn/test/jest';
|
||||
|
||||
import { AddResultLogic } from './';
|
||||
|
||||
describe('AddResultLogic', () => {
|
||||
const { mount } = new LogicMounter(AddResultLogic);
|
||||
const { http } = mockHttpValues;
|
||||
const { flashAPIErrors } = mockFlashMessageHelpers;
|
||||
|
||||
const MOCK_SEARCH_RESPONSE = {
|
||||
results: [
|
||||
{ id: { raw: 'document-1' }, _meta: { id: 'document-1', engine: 'some-engine' } },
|
||||
{ id: { raw: 'document-2' }, _meta: { id: 'document-2', engine: 'some-engine' } },
|
||||
{ id: { raw: 'document-3' }, _meta: { id: 'document-3', engine: 'some-engine' } },
|
||||
],
|
||||
};
|
||||
|
||||
const DEFAULT_VALUES = {
|
||||
isFlyoutOpen: false,
|
||||
dataLoading: false,
|
||||
searchQuery: '',
|
||||
searchResults: [],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -51,7 +36,6 @@ describe('AddResultLogic', () => {
|
|||
expect(AddResultLogic.values).toEqual({
|
||||
...DEFAULT_VALUES,
|
||||
isFlyoutOpen: true,
|
||||
searchQuery: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -68,67 +52,5 @@ describe('AddResultLogic', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('search', () => {
|
||||
it('sets searchQuery & dataLoading to true', () => {
|
||||
mount({ searchQuery: '', dataLoading: false });
|
||||
|
||||
AddResultLogic.actions.search('hello world');
|
||||
|
||||
expect(AddResultLogic.values).toEqual({
|
||||
...DEFAULT_VALUES,
|
||||
searchQuery: 'hello world',
|
||||
dataLoading: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('onSearch', () => {
|
||||
it('sets searchResults & dataLoading to false', () => {
|
||||
mount({ searchResults: [], dataLoading: true });
|
||||
|
||||
AddResultLogic.actions.onSearch(MOCK_SEARCH_RESPONSE);
|
||||
|
||||
expect(AddResultLogic.values).toEqual({
|
||||
...DEFAULT_VALUES,
|
||||
searchResults: MOCK_SEARCH_RESPONSE.results,
|
||||
dataLoading: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('listeners', () => {
|
||||
describe('search', () => {
|
||||
beforeAll(() => jest.useFakeTimers());
|
||||
afterAll(() => jest.useRealTimers());
|
||||
|
||||
it('should make a GET API call with a search query', async () => {
|
||||
http.get.mockReturnValueOnce(Promise.resolve(MOCK_SEARCH_RESPONSE));
|
||||
mount();
|
||||
jest.spyOn(AddResultLogic.actions, 'onSearch');
|
||||
|
||||
AddResultLogic.actions.search('hello world');
|
||||
jest.runAllTimers();
|
||||
await nextTick();
|
||||
|
||||
expect(http.get).toHaveBeenCalledWith(
|
||||
'/api/app_search/engines/some-engine/curation_search',
|
||||
{ query: { query: 'hello world' } }
|
||||
);
|
||||
expect(AddResultLogic.actions.onSearch).toHaveBeenCalledWith(MOCK_SEARCH_RESPONSE);
|
||||
});
|
||||
|
||||
it('handles errors', async () => {
|
||||
http.get.mockReturnValueOnce(Promise.reject('error'));
|
||||
mount();
|
||||
|
||||
AddResultLogic.actions.search('test');
|
||||
jest.runAllTimers();
|
||||
await nextTick();
|
||||
|
||||
expect(flashAPIErrors).toHaveBeenCalledWith('error');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,24 +7,13 @@
|
|||
|
||||
import { kea, MakeLogicType } from 'kea';
|
||||
|
||||
import { flashAPIErrors } from '../../../../../shared/flash_messages';
|
||||
import { HttpLogic } from '../../../../../shared/http';
|
||||
|
||||
import { EngineLogic } from '../../../engine';
|
||||
import { Result } from '../../../result/types';
|
||||
|
||||
interface AddResultValues {
|
||||
isFlyoutOpen: boolean;
|
||||
dataLoading: boolean;
|
||||
searchQuery: string;
|
||||
searchResults: Result[];
|
||||
}
|
||||
|
||||
interface AddResultActions {
|
||||
openFlyout(): void;
|
||||
closeFlyout(): void;
|
||||
search(query: string): { query: string };
|
||||
onSearch({ results }: { results: Result[] }): { results: Result[] };
|
||||
}
|
||||
|
||||
export const AddResultLogic = kea<MakeLogicType<AddResultValues, AddResultActions>>({
|
||||
|
@ -32,8 +21,6 @@ export const AddResultLogic = kea<MakeLogicType<AddResultValues, AddResultAction
|
|||
actions: () => ({
|
||||
openFlyout: true,
|
||||
closeFlyout: true,
|
||||
search: (query) => ({ query }),
|
||||
onSearch: ({ results }) => ({ results }),
|
||||
}),
|
||||
reducers: () => ({
|
||||
isFlyoutOpen: [
|
||||
|
@ -43,42 +30,5 @@ export const AddResultLogic = kea<MakeLogicType<AddResultValues, AddResultAction
|
|||
closeFlyout: () => false,
|
||||
},
|
||||
],
|
||||
dataLoading: [
|
||||
false,
|
||||
{
|
||||
search: () => true,
|
||||
onSearch: () => false,
|
||||
},
|
||||
],
|
||||
searchQuery: [
|
||||
'',
|
||||
{
|
||||
search: (_, { query }) => query,
|
||||
openFlyout: () => '',
|
||||
},
|
||||
],
|
||||
searchResults: [
|
||||
[],
|
||||
{
|
||||
onSearch: (_, { results }) => results,
|
||||
},
|
||||
],
|
||||
}),
|
||||
listeners: ({ actions }) => ({
|
||||
search: async ({ query }, breakpoint) => {
|
||||
await breakpoint(250);
|
||||
|
||||
const { http } = HttpLogic.values;
|
||||
const { engineName } = EngineLogic.values;
|
||||
|
||||
try {
|
||||
const response = await http.get(`/api/app_search/engines/${engineName}/curation_search`, {
|
||||
query: { query },
|
||||
});
|
||||
actions.onSearch(response);
|
||||
} catch (e) {
|
||||
flashAPIErrors(e);
|
||||
}
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -11,7 +11,7 @@ import React from 'react';
|
|||
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { EuiButtonEmpty } from '@elastic/eui';
|
||||
import { QueryTesterButton } from '../query_tester';
|
||||
|
||||
import { KibanaHeaderActions } from './kibana_header_actions';
|
||||
|
||||
|
@ -27,7 +27,7 @@ describe('KibanaHeaderActions', () => {
|
|||
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<KibanaHeaderActions />);
|
||||
expect(wrapper.find(EuiButtonEmpty).exists()).toBe(true);
|
||||
expect(wrapper.find(QueryTesterButton).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not render a "Query Tester" button if there is no engine available', () => {
|
||||
|
@ -35,6 +35,6 @@ describe('KibanaHeaderActions', () => {
|
|||
engineName: '',
|
||||
});
|
||||
const wrapper = shallow(<KibanaHeaderActions />);
|
||||
expect(wrapper.find(EuiButtonEmpty).exists()).toBe(false);
|
||||
expect(wrapper.find(QueryTesterButton).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,10 +9,10 @@ import React from 'react';
|
|||
|
||||
import { useValues } from 'kea';
|
||||
|
||||
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
|
||||
import { EngineLogic } from '../engine';
|
||||
import { QueryTesterButton } from '../query_tester';
|
||||
|
||||
export const KibanaHeaderActions: React.FC = () => {
|
||||
const { engineName } = useValues(EngineLogic);
|
||||
|
@ -21,11 +21,7 @@ export const KibanaHeaderActions: React.FC = () => {
|
|||
<EuiFlexGroup gutterSize="s">
|
||||
{engineName && (
|
||||
<EuiFlexItem>
|
||||
<EuiButtonEmpty iconType="beaker" size="s">
|
||||
{i18n.translate('xpack.enterpriseSearch.appSearch.engine.queryTesterButtonLabel', {
|
||||
defaultMessage: 'Query tester',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
<QueryTesterButton />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const QUERY_TESTER_TITLE = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.queryTesterTitle',
|
||||
{
|
||||
defaultMessage: 'Query tester',
|
||||
}
|
||||
);
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { QueryTesterFlyout } from './query_tester_flyout';
|
||||
export { QueryTesterButton } from './query_tester_button';
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { setMockValues, setMockActions } from '../../../__mocks__/kea.mock';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { EuiEmptyPrompt, EuiFieldSearch } from '@elastic/eui';
|
||||
|
||||
import { SchemaType } from '../../../shared/schema/types';
|
||||
import { Result } from '../result';
|
||||
|
||||
import { QueryTester } from './query_tester';
|
||||
|
||||
describe('QueryTester', () => {
|
||||
const values = {
|
||||
searchQuery: 'foo',
|
||||
searchResults: [{ id: { raw: '1' } }, { id: { raw: '2' } }, { id: { raw: '3' } }],
|
||||
searchDataLoading: false,
|
||||
engine: {
|
||||
schema: {
|
||||
foo: SchemaType.Text,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const actions = {
|
||||
search: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
setMockValues(values);
|
||||
setMockActions(actions);
|
||||
});
|
||||
|
||||
it('renders with a search box and results', () => {
|
||||
const wrapper = shallow(<QueryTester />);
|
||||
expect(wrapper.find(EuiFieldSearch).prop('value')).toBe('foo');
|
||||
expect(wrapper.find(EuiFieldSearch).prop('isLoading')).toBe(false);
|
||||
expect(wrapper.find(Result)).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('will update the search term in state when the user updates the search box', () => {
|
||||
const wrapper = shallow(<QueryTester />);
|
||||
wrapper.find(EuiFieldSearch).simulate('change', { target: { value: 'bar' } });
|
||||
expect(actions.search).toHaveBeenCalledWith('bar');
|
||||
});
|
||||
|
||||
it('will render an empty prompt when there are no results', () => {
|
||||
setMockValues({
|
||||
...values,
|
||||
searchResults: [],
|
||||
});
|
||||
const wrapper = shallow(<QueryTester />);
|
||||
wrapper.find(EuiFieldSearch).simulate('change', { target: { value: 'bar' } });
|
||||
expect(wrapper.find(Result)).toHaveLength(0);
|
||||
expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { useActions, useValues } from 'kea';
|
||||
|
||||
import { EuiEmptyPrompt, EuiFieldSearch, EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { EngineLogic } from '../engine';
|
||||
import { Result } from '../result';
|
||||
import { SearchLogic } from '../search';
|
||||
|
||||
export const QueryTester: React.FC = () => {
|
||||
const logic = SearchLogic({ id: 'query-tester' });
|
||||
const { searchQuery, searchResults, searchDataLoading } = useValues(logic);
|
||||
const { search } = useActions(logic);
|
||||
const { engine } = useValues(EngineLogic);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFieldSearch
|
||||
value={searchQuery}
|
||||
onChange={(e) => search(e.target.value)}
|
||||
isLoading={searchDataLoading}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.queryTester.searchPlaceholder',
|
||||
{ defaultMessage: 'Search engine documents' }
|
||||
)}
|
||||
fullWidth
|
||||
autoFocus
|
||||
/>
|
||||
<EuiSpacer />
|
||||
{searchResults.length > 0 ? (
|
||||
searchResults.map((result) => {
|
||||
const id = result.id.raw;
|
||||
|
||||
return (
|
||||
<React.Fragment key={id}>
|
||||
<Result
|
||||
isMetaEngine={false}
|
||||
key={id}
|
||||
result={result}
|
||||
schemaForTypeHighlights={engine.schema}
|
||||
showScore
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
</React.Fragment>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<EuiEmptyPrompt
|
||||
body={i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.curations.addResult.searchEmptyDescription',
|
||||
{ defaultMessage: 'No matching content found.' }
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { EuiButtonEmpty } from '@elastic/eui';
|
||||
|
||||
import { QueryTesterFlyout, QueryTesterButton } from '.';
|
||||
|
||||
describe('QueryTesterButton', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<QueryTesterButton />);
|
||||
expect(wrapper.find(EuiButtonEmpty).exists()).toBe(true);
|
||||
expect(wrapper.find(QueryTesterFlyout).exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('will render a QueryTesterFlyout when pressed and close on QueryTesterFlyout close', () => {
|
||||
const wrapper = shallow(<QueryTesterButton />);
|
||||
wrapper.find(EuiButtonEmpty).simulate('click');
|
||||
expect(wrapper.find(QueryTesterFlyout).exists()).toBe(true);
|
||||
|
||||
wrapper.find(QueryTesterFlyout).simulate('close');
|
||||
expect(wrapper.find(QueryTesterFlyout).exists()).toBe(false);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { EuiButtonEmpty } from '@elastic/eui';
|
||||
|
||||
import { QUERY_TESTER_TITLE } from './i18n';
|
||||
|
||||
import { QueryTesterFlyout } from '.';
|
||||
|
||||
export const QueryTesterButton: React.FC = () => {
|
||||
const [isQueryTesterOpen, setIsQueryTesterOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<EuiButtonEmpty
|
||||
iconType="beaker"
|
||||
size="s"
|
||||
onClick={() => setIsQueryTesterOpen(!isQueryTesterOpen)}
|
||||
>
|
||||
{QUERY_TESTER_TITLE}
|
||||
</EuiButtonEmpty>
|
||||
{isQueryTesterOpen && <QueryTesterFlyout onClose={() => setIsQueryTesterOpen(false)} />}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { EuiFlyout } from '@elastic/eui';
|
||||
|
||||
import { QueryTester } from './query_tester';
|
||||
import { QueryTesterFlyout } from './query_tester_flyout';
|
||||
|
||||
describe('QueryTesterFlyout', () => {
|
||||
const onClose = jest.fn();
|
||||
|
||||
it('renders', () => {
|
||||
const wrapper = shallow(<QueryTesterFlyout onClose={onClose} />);
|
||||
expect(wrapper.find(QueryTester).exists()).toBe(true);
|
||||
expect(wrapper.find(EuiFlyout).prop('onClose')).toEqual(onClose);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui';
|
||||
|
||||
import { QUERY_TESTER_TITLE } from './i18n';
|
||||
import { QueryTester } from './query_tester';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const QueryTesterFlyout: React.FC<Props> = ({ onClose }) => {
|
||||
return (
|
||||
<EuiFlyout onClose={onClose} aria-labelledby="queryTesterFlyoutTitle" ownFocus>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<h2 id="queryTesterFlyoutTitle">{QUERY_TESTER_TITLE}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<QueryTester />
|
||||
</EuiFlyoutBody>
|
||||
</EuiFlyout>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { SearchLogic } from './search_logic';
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import '../../__mocks__/engine_logic.mock';
|
||||
|
||||
import { LogicMounter, mockHttpValues, mockFlashMessageHelpers } from '../../../__mocks__';
|
||||
|
||||
import { nextTick } from '@kbn/test/jest';
|
||||
|
||||
import { SearchLogic } from './search_logic';
|
||||
|
||||
describe('SearchLogic', () => {
|
||||
const { mount } = new LogicMounter(SearchLogic);
|
||||
const { http } = mockHttpValues;
|
||||
const { flashAPIErrors } = mockFlashMessageHelpers;
|
||||
|
||||
const MOCK_SEARCH_RESPONSE = {
|
||||
results: [
|
||||
{ id: { raw: 'document-1' }, _meta: { id: 'document-1', engine: 'some-engine' } },
|
||||
{ id: { raw: 'document-2' }, _meta: { id: 'document-2', engine: 'some-engine' } },
|
||||
{ id: { raw: 'document-3' }, _meta: { id: 'document-3', engine: 'some-engine' } },
|
||||
],
|
||||
};
|
||||
|
||||
const DEFAULT_VALUES = {
|
||||
searchDataLoading: false,
|
||||
searchQuery: '',
|
||||
searchResults: [],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const mountLogic = (values: object = {}) => mount(values, { id: '1' });
|
||||
|
||||
it('has expected default values', () => {
|
||||
const logic = mountLogic();
|
||||
expect(logic.values).toEqual(DEFAULT_VALUES);
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
describe('search', () => {
|
||||
it('sets searchQuery & searchDataLoading to true', () => {
|
||||
const logic = mountLogic({ searchQuery: '', searchDataLoading: false });
|
||||
|
||||
logic.actions.search('hello world');
|
||||
|
||||
expect(logic.values).toEqual({
|
||||
...DEFAULT_VALUES,
|
||||
searchQuery: 'hello world',
|
||||
searchDataLoading: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('onSearch', () => {
|
||||
it('sets searchResults & searchDataLoading to false', () => {
|
||||
const logic = mountLogic({ searchResults: [], searchDataLoading: true });
|
||||
|
||||
logic.actions.onSearch(MOCK_SEARCH_RESPONSE);
|
||||
|
||||
expect(logic.values).toEqual({
|
||||
...DEFAULT_VALUES,
|
||||
searchResults: MOCK_SEARCH_RESPONSE.results,
|
||||
searchDataLoading: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('listeners', () => {
|
||||
describe('search', () => {
|
||||
beforeAll(() => jest.useFakeTimers());
|
||||
afterAll(() => jest.useRealTimers());
|
||||
|
||||
it('should make a GET API call with a search query', async () => {
|
||||
http.get.mockReturnValueOnce(Promise.resolve(MOCK_SEARCH_RESPONSE));
|
||||
const logic = mountLogic();
|
||||
jest.spyOn(logic.actions, 'onSearch');
|
||||
|
||||
logic.actions.search('hello world');
|
||||
jest.runAllTimers();
|
||||
await nextTick();
|
||||
|
||||
expect(http.get).toHaveBeenCalledWith('/api/app_search/engines/some-engine/search', {
|
||||
query: { query: 'hello world' },
|
||||
});
|
||||
expect(logic.actions.onSearch).toHaveBeenCalledWith(MOCK_SEARCH_RESPONSE);
|
||||
});
|
||||
|
||||
it('handles errors', async () => {
|
||||
http.get.mockReturnValueOnce(Promise.reject('error'));
|
||||
const logic = mountLogic();
|
||||
|
||||
logic.actions.search('test');
|
||||
jest.runAllTimers();
|
||||
await nextTick();
|
||||
|
||||
expect(flashAPIErrors).toHaveBeenCalledWith('error');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { kea, MakeLogicType } from 'kea';
|
||||
|
||||
import { flashAPIErrors } from '../../../shared/flash_messages';
|
||||
|
||||
import { HttpLogic } from '../../../shared/http';
|
||||
import { EngineLogic } from '../engine';
|
||||
|
||||
import { Result } from '../result/types';
|
||||
|
||||
interface SearchValues {
|
||||
searchDataLoading: boolean;
|
||||
searchQuery: string;
|
||||
searchResults: Result[];
|
||||
}
|
||||
|
||||
interface SearchActions {
|
||||
search(query: string): { query: string };
|
||||
onSearch({ results }: { results: Result[] }): { results: Result[] };
|
||||
}
|
||||
|
||||
export const SearchLogic = kea<MakeLogicType<SearchValues, SearchActions>>({
|
||||
key: (props) => props.id,
|
||||
path: (key: string) => ['enterprise_search', 'app_search', 'search_logic', key],
|
||||
actions: () => ({
|
||||
search: (query) => ({ query }),
|
||||
onSearch: ({ results }) => ({ results }),
|
||||
}),
|
||||
reducers: () => ({
|
||||
searchDataLoading: [
|
||||
false,
|
||||
{
|
||||
search: () => true,
|
||||
onSearch: () => false,
|
||||
},
|
||||
],
|
||||
searchQuery: [
|
||||
'',
|
||||
{
|
||||
search: (_, { query }) => query,
|
||||
},
|
||||
],
|
||||
searchResults: [
|
||||
[],
|
||||
{
|
||||
onSearch: (_, { results }) => results,
|
||||
},
|
||||
],
|
||||
}),
|
||||
listeners: ({ actions }) => ({
|
||||
search: async ({ query }, breakpoint) => {
|
||||
await breakpoint(250);
|
||||
|
||||
const { http } = HttpLogic.values;
|
||||
const { engineName } = EngineLogic.values;
|
||||
|
||||
try {
|
||||
const response = await http.get(`/api/app_search/engines/${engineName}/search`, {
|
||||
query: { query },
|
||||
});
|
||||
actions.onSearch(response);
|
||||
} catch (e) {
|
||||
flashAPIErrors(e);
|
||||
}
|
||||
},
|
||||
}),
|
||||
});
|
|
@ -229,39 +229,4 @@ describe('curations routes', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/app_search/engines/{engineName}/curation_search', () => {
|
||||
let mockRouter: MockRouter;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockRouter = new MockRouter({
|
||||
method: 'get',
|
||||
path: '/api/app_search/engines/{engineName}/curation_search',
|
||||
});
|
||||
|
||||
registerCurationsRoutes({
|
||||
...mockDependencies,
|
||||
router: mockRouter.router,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a request handler', () => {
|
||||
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
|
||||
path: '/api/as/v1/engines/:engineName/search.json',
|
||||
});
|
||||
});
|
||||
|
||||
describe('validates', () => {
|
||||
it('required query param', () => {
|
||||
const request = { query: { query: 'some query' } };
|
||||
mockRouter.shouldValidate(request);
|
||||
});
|
||||
|
||||
it('missing query', () => {
|
||||
const request = { query: {} };
|
||||
mockRouter.shouldThrow(request);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,6 +17,7 @@ import { registerOnboardingRoutes } from './onboarding';
|
|||
import { registerResultSettingsRoutes } from './result_settings';
|
||||
import { registerRoleMappingsRoutes } from './role_mappings';
|
||||
import { registerSchemaRoutes } from './schema';
|
||||
import { registerSearchRoutes } from './search';
|
||||
import { registerSearchSettingsRoutes } from './search_settings';
|
||||
import { registerSearchUIRoutes } from './search_ui';
|
||||
import { registerSettingsRoutes } from './settings';
|
||||
|
@ -31,6 +32,7 @@ export const registerAppSearchRoutes = (dependencies: RouteDependencies) => {
|
|||
registerDocumentsRoutes(dependencies);
|
||||
registerDocumentRoutes(dependencies);
|
||||
registerSchemaRoutes(dependencies);
|
||||
registerSearchRoutes(dependencies);
|
||||
registerSourceEnginesRoutes(dependencies);
|
||||
registerCurationsRoutes(dependencies);
|
||||
registerSynonymsRoutes(dependencies);
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__';
|
||||
|
||||
import { registerSearchRoutes } from './search';
|
||||
|
||||
describe('search routes', () => {
|
||||
describe('GET /api/app_search/engines/{engineName}/search', () => {
|
||||
let mockRouter: MockRouter;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockRouter = new MockRouter({
|
||||
method: 'get',
|
||||
path: '/api/app_search/engines/{engineName}/schema',
|
||||
});
|
||||
|
||||
registerSearchRoutes({
|
||||
...mockDependencies,
|
||||
router: mockRouter.router,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a request handler', () => {
|
||||
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
|
||||
path: '/api/as/v1/engines/:engineName/search.json',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
import { RouteDependencies } from '../../plugin';
|
||||
|
||||
export function registerSearchRoutes({
|
||||
router,
|
||||
enterpriseSearchRequestHandler,
|
||||
}: RouteDependencies) {
|
||||
router.get(
|
||||
{
|
||||
path: '/api/app_search/engines/{engineName}/search',
|
||||
validate: {
|
||||
params: schema.object({
|
||||
engineName: schema.string(),
|
||||
}),
|
||||
query: schema.object({
|
||||
query: schema.string(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
enterpriseSearchRequestHandler.createRequest({
|
||||
path: '/api/as/v1/engines/:engineName/search.json',
|
||||
})
|
||||
);
|
||||
}
|
Loading…
Reference in a new issue