mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
* Adds base FE implementation for documents list
* Add backend for pagination.
* Add tests
* Use pagination instead of meta
* Use 0 indexed pagination
* Fix axe error
(cherry picked from commit e3760c580a
)
Co-authored-by: Efe Gürkan YALAMAN <efeguerkan.yalaman@elastic.co>
This commit is contained in:
parent
77678c4c0f
commit
01287cbccc
11 changed files with 674 additions and 71 deletions
|
@ -96,3 +96,5 @@ export const ENTERPRISE_SEARCH_AUDIT_LOGS_SOURCE_ID = 'ent-search-audit-logs';
|
|||
export const APP_SEARCH_URL = '/app/enterprise_search/app_search';
|
||||
export const ENTERPRISE_SEARCH_ELASTICSEARCH_URL = '/app/enterprise_search/elasticsearch';
|
||||
export const WORKPLACE_SEARCH_URL = '/app/enterprise_search/workplace_search';
|
||||
|
||||
export const ENTERPRISE_SEARCH_DOCUMENTS_DEFAULT_DOC_COUNT = 25;
|
||||
|
|
|
@ -7,19 +7,31 @@
|
|||
|
||||
import { SearchResponseBody } from '@elastic/elasticsearch/lib/api/types';
|
||||
|
||||
import { Meta } from '../../../../../common/types';
|
||||
|
||||
import { createApiLogic } from '../../../shared/api_logic/create_api_logic';
|
||||
import { HttpLogic } from '../../../shared/http';
|
||||
|
||||
export const searchDocuments = async ({
|
||||
docsPerPage,
|
||||
indexName,
|
||||
query,
|
||||
pagination,
|
||||
query: q,
|
||||
}: {
|
||||
docsPerPage?: number;
|
||||
indexName: string;
|
||||
pagination: { pageIndex: number; pageSize: number; totalItemCount: number };
|
||||
query: string;
|
||||
}) => {
|
||||
const route = `/internal/enterprise_search/indices/${indexName}/search/${query}`;
|
||||
const route = `/internal/enterprise_search/indices/${indexName}/search/${q}`;
|
||||
const query = {
|
||||
page: pagination.pageIndex,
|
||||
size: docsPerPage || pagination.pageSize,
|
||||
};
|
||||
|
||||
return await HttpLogic.values.http.get<SearchResponseBody>(route);
|
||||
return await HttpLogic.values.http.get<{ meta: Meta; results: SearchResponseBody }>(route, {
|
||||
query,
|
||||
});
|
||||
};
|
||||
|
||||
export const SearchDocumentsApiLogic = createApiLogic(
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* 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_logic';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { EuiCallOut, EuiPagination } from '@elastic/eui';
|
||||
|
||||
import { Status } from '../../../../../../../common/types/api';
|
||||
|
||||
import { Result } from '../../../../../shared/result/result';
|
||||
|
||||
import { INDEX_DOCUMENTS_META_DEFAULT } from '../../documents_logic';
|
||||
|
||||
import { DocumentList } from './document_list';
|
||||
|
||||
const mockActions = {};
|
||||
|
||||
export const DEFAULT_VALUES = {
|
||||
data: undefined,
|
||||
indexName: 'indexName',
|
||||
isLoading: true,
|
||||
mappingData: undefined,
|
||||
mappingStatus: 0,
|
||||
meta: INDEX_DOCUMENTS_META_DEFAULT,
|
||||
query: '',
|
||||
results: [],
|
||||
status: Status.IDLE,
|
||||
};
|
||||
|
||||
const mockValues = { ...DEFAULT_VALUES };
|
||||
|
||||
describe('DocumentList', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
setMockValues(mockValues);
|
||||
setMockActions(mockActions);
|
||||
});
|
||||
it('renders empty', () => {
|
||||
const wrapper = shallow(<DocumentList />);
|
||||
expect(wrapper.find(Result)).toHaveLength(0);
|
||||
expect(wrapper.find(EuiPagination)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('renders documents when results when there is data and mappings', () => {
|
||||
setMockValues({
|
||||
...mockValues,
|
||||
results: [
|
||||
{
|
||||
_id: 'M9ntXoIBTq5dF-1Xnc8A',
|
||||
_index: 'kibana_sample_data_flights',
|
||||
_score: 1,
|
||||
_source: {
|
||||
AvgTicketPrice: 268.24159591388866,
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: 'NNntXoIBTq5dF-1Xnc8A',
|
||||
_index: 'kibana_sample_data_flights',
|
||||
_score: 1,
|
||||
_source: {
|
||||
AvgTicketPrice: 68.91388866,
|
||||
},
|
||||
},
|
||||
],
|
||||
simplifiedMapping: {
|
||||
AvgTicketPrice: {
|
||||
type: 'float',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = shallow(<DocumentList />);
|
||||
expect(wrapper.find(Result)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('renders callout when total results are 10.000', () => {
|
||||
setMockValues({
|
||||
...mockValues,
|
||||
meta: {
|
||||
page: {
|
||||
...INDEX_DOCUMENTS_META_DEFAULT.page,
|
||||
total_results: 10000,
|
||||
},
|
||||
},
|
||||
});
|
||||
const wrapper = shallow(<DocumentList />);
|
||||
expect(wrapper.find(EuiCallOut)).toHaveLength(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,207 @@
|
|||
/*
|
||||
* 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 { useActions, useValues } from 'kea';
|
||||
|
||||
import { SearchHit } from '@elastic/elasticsearch/lib/api/types';
|
||||
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiCallOut,
|
||||
EuiContextMenuItem,
|
||||
EuiContextMenuPanel,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPagination,
|
||||
EuiProgress,
|
||||
EuiPopover,
|
||||
EuiText,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { Result } from '../../../../../shared/result/result';
|
||||
|
||||
import { DocumentsLogic } from '../../documents_logic';
|
||||
|
||||
export const DocumentList: React.FC = () => {
|
||||
const {
|
||||
docsPerPage,
|
||||
isLoading,
|
||||
meta,
|
||||
results,
|
||||
simplifiedMapping: mappings,
|
||||
} = useValues(DocumentsLogic);
|
||||
const { onPaginate, setDocsPerPage } = useActions(DocumentsLogic);
|
||||
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const resultToField = (result: SearchHit) => {
|
||||
if (mappings && result._source && !Array.isArray(result._source)) {
|
||||
if (typeof result._source === 'object') {
|
||||
return Object.entries(result._source).map(([key, value]) => {
|
||||
return {
|
||||
fieldName: key,
|
||||
fieldType: mappings[key]?.type ?? 'object',
|
||||
fieldValue: JSON.stringify(value, null, 2),
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const docsPerPageButton = (
|
||||
<EuiButtonEmpty
|
||||
size="s"
|
||||
iconType="arrowDown"
|
||||
iconSide="right"
|
||||
onClick={() => {
|
||||
setIsPopoverOpen(true);
|
||||
}}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.content.searchIndex.documents.documentList.pagination.itemsPerPage',
|
||||
{
|
||||
defaultMessage: 'Documents per page: {docPerPage}',
|
||||
values: { docPerPage: docsPerPage },
|
||||
}
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
||||
const getIconType = (size: number) => {
|
||||
return size === docsPerPage ? 'check' : 'empty';
|
||||
};
|
||||
|
||||
const docsPerPageOptions = [
|
||||
<EuiContextMenuItem
|
||||
key="10 rows"
|
||||
icon={getIconType(10)}
|
||||
onClick={() => {
|
||||
setIsPopoverOpen(false);
|
||||
setDocsPerPage(10);
|
||||
}}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.content.searchIndex.documents.documentList.paginationOptions.option',
|
||||
{ defaultMessage: '{docCount} documents', values: { docCount: 10 } }
|
||||
)}
|
||||
</EuiContextMenuItem>,
|
||||
|
||||
<EuiContextMenuItem
|
||||
key="25 rows"
|
||||
icon={getIconType(25)}
|
||||
onClick={() => {
|
||||
setIsPopoverOpen(false);
|
||||
setDocsPerPage(25);
|
||||
}}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.content.searchIndex.documents.documentList.paginationOptions.option',
|
||||
{ defaultMessage: '{docCount} documents', values: { docCount: 25 } }
|
||||
)}
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
key="50 rows"
|
||||
icon={getIconType(50)}
|
||||
onClick={() => {
|
||||
setIsPopoverOpen(false);
|
||||
setDocsPerPage(50);
|
||||
}}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.content.searchIndex.documents.documentList.paginationOptions.option',
|
||||
{ defaultMessage: '{docCount} documents', values: { docCount: 50 } }
|
||||
)}
|
||||
</EuiContextMenuItem>,
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiPagination
|
||||
aria-label={i18n.translate(
|
||||
'xpack.enterpriseSearch.content.searchIndex.documents.documentList.paginationAriaLabel',
|
||||
{ defaultMessage: 'Pagination for document list' }
|
||||
)}
|
||||
pageCount={meta.page.total_pages}
|
||||
activePage={meta.page.current}
|
||||
onPageClick={onPaginate}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiText size="xs">
|
||||
<p>
|
||||
Showing <strong>{results.length}</strong> of <strong>{meta.page.total_results}</strong>.
|
||||
Search results maxed at 10.000 documents.
|
||||
</p>
|
||||
</EuiText>
|
||||
{isLoading && <EuiProgress size="xs" color="primary" />}
|
||||
<EuiSpacer size="m" />
|
||||
{results.map((result) => {
|
||||
return (
|
||||
<React.Fragment key={result._id}>
|
||||
<Result
|
||||
fields={resultToField(result)}
|
||||
metaData={{
|
||||
id: result._id,
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPagination
|
||||
aria-label={i18n.translate(
|
||||
'xpack.enterpriseSearch.content.searchIndex.documents.documentList.paginationAriaLabel',
|
||||
{ defaultMessage: 'Pagination for document list' }
|
||||
)}
|
||||
pageCount={meta.page.total_pages}
|
||||
activePage={meta.page.current}
|
||||
onPageClick={onPaginate}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
aria-label={i18n.translate(
|
||||
'xpack.enterpriseSearch.content.searchIndex.documents.documentList.docsPerPage',
|
||||
{ defaultMessage: 'Document count per page dropdown' }
|
||||
)}
|
||||
button={docsPerPageButton}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={() => {
|
||||
setIsPopoverOpen(false);
|
||||
}}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downLeft"
|
||||
>
|
||||
<EuiContextMenuPanel size="s" items={docsPerPageOptions} />
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer />
|
||||
{meta.page.total_results === 10000 && (
|
||||
<EuiCallOut size="s" title="Results are limited to 10.000 documents" iconType="search">
|
||||
<p>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.content.searchIndex.documents.documentList.resultLimit',
|
||||
{
|
||||
defaultMessage:
|
||||
'Only the first 10,000 results are available for paging. Please use the search bar to filter down your results.',
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -9,53 +9,40 @@ import React, { useEffect, ChangeEvent } from 'react';
|
|||
|
||||
import { useActions, useValues } from 'kea';
|
||||
|
||||
import { SearchHit } from '@elastic/elasticsearch/lib/api/types';
|
||||
|
||||
import {
|
||||
EuiFieldSearch,
|
||||
EuiTitle,
|
||||
EuiSpacer,
|
||||
EuiPanel,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { Result } from '../../../shared/result/result';
|
||||
import { DocumentList } from './components/document_list/document_list';
|
||||
import { DocumentsLogic, DEFAULT_PAGINATION } from './documents_logic';
|
||||
|
||||
import { DocumentsLogic } from './documents_logic';
|
||||
import { IndexNameLogic } from './index_name_logic';
|
||||
|
||||
import './documents.scss';
|
||||
|
||||
export const SearchIndexDocuments: React.FC = () => {
|
||||
const { indexName } = useValues(IndexNameLogic);
|
||||
const { simplifiedMapping, results } = useValues(DocumentsLogic);
|
||||
const { simplifiedMapping } = useValues(DocumentsLogic);
|
||||
const { makeRequest, makeMappingRequest, setSearchQuery } = useActions(DocumentsLogic);
|
||||
|
||||
useEffect(() => {
|
||||
makeRequest({ indexName, query: '' });
|
||||
makeRequest({
|
||||
indexName,
|
||||
pagination: DEFAULT_PAGINATION,
|
||||
query: '',
|
||||
});
|
||||
makeMappingRequest({ indexName });
|
||||
}, [indexName]);
|
||||
|
||||
const resultToField = (result: SearchHit) => {
|
||||
if (simplifiedMapping && result._source && !Array.isArray(result._source)) {
|
||||
if (typeof result._source === 'object') {
|
||||
return Object.entries(result._source).map(([key, value]) => {
|
||||
return {
|
||||
fieldName: key,
|
||||
fieldType: simplifiedMapping[key]?.type ?? 'object',
|
||||
fieldValue: JSON.stringify(value, null, 2),
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiPanel hasBorder={false} hasShadow={false}>
|
||||
<EuiPanel hasBorder={false} hasShadow={false} paddingSize="none">
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
|
@ -91,21 +78,7 @@ export const SearchIndexDocuments: React.FC = () => {
|
|||
i18n.translate('xpack.enterpriseSearch.content.searchIndex.documents.noMappings', {
|
||||
defaultMessage: 'No mappings found for index',
|
||||
})}
|
||||
|
||||
{simplifiedMapping &&
|
||||
results.map((result) => {
|
||||
return (
|
||||
<>
|
||||
<Result
|
||||
fields={resultToField(result)}
|
||||
metaData={{
|
||||
id: result._id,
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
);
|
||||
})}
|
||||
{simplifiedMapping && <DocumentList />}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
|
|
|
@ -10,19 +10,24 @@ import { LogicMounter, mockFlashMessageHelpers } from '../../../__mocks__/kea_lo
|
|||
import { nextTick } from '@kbn/test-jest-helpers';
|
||||
|
||||
import { HttpError, Status } from '../../../../../common/types/api';
|
||||
|
||||
import { MappingsApiLogic } from '../../api/mappings/mappings_logic';
|
||||
import { SearchDocumentsApiLogic } from '../../api/search_documents/search_documents_logic';
|
||||
|
||||
import { DocumentsLogic } from './documents_logic';
|
||||
import {
|
||||
DocumentsLogic,
|
||||
INDEX_DOCUMENTS_META_DEFAULT,
|
||||
convertMetaToPagination,
|
||||
} from './documents_logic';
|
||||
import { IndexNameLogic } from './index_name_logic';
|
||||
|
||||
const DEFAULT_VALUES = {
|
||||
export const DEFAULT_VALUES = {
|
||||
data: undefined,
|
||||
docsPerPage: 25,
|
||||
indexName: 'indexName',
|
||||
isLoading: true,
|
||||
mappingData: undefined,
|
||||
mappingStatus: 0,
|
||||
meta: INDEX_DOCUMENTS_META_DEFAULT,
|
||||
query: '',
|
||||
results: [],
|
||||
status: Status.IDLE,
|
||||
|
@ -57,6 +62,25 @@ describe('DocumentsLogic', () => {
|
|||
expect(DocumentsLogic.values).toEqual({ ...DEFAULT_VALUES, query: newQueryString });
|
||||
});
|
||||
});
|
||||
describe('setDocsPerPage', () => {
|
||||
it('sets documents to show per page', () => {
|
||||
const docsToShow = 50;
|
||||
expect(DocumentsLogic.values).toEqual({ ...DEFAULT_VALUES });
|
||||
DocumentsLogic.actions.setDocsPerPage(docsToShow);
|
||||
expect(DocumentsLogic.values).toEqual({
|
||||
...DEFAULT_VALUES,
|
||||
docsPerPage: docsToShow,
|
||||
meta: {
|
||||
page: {
|
||||
...INDEX_DOCUMENTS_META_DEFAULT.page,
|
||||
size: docsToShow,
|
||||
},
|
||||
},
|
||||
simplifiedMapping: undefined,
|
||||
status: Status.LOADING,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('listeners', () => {
|
||||
describe('setSearchQuery', () => {
|
||||
|
@ -69,7 +93,9 @@ describe('DocumentsLogic', () => {
|
|||
jest.advanceTimersByTime(250);
|
||||
await nextTick();
|
||||
expect(DocumentsLogic.actions.makeRequest).toHaveBeenCalledWith({
|
||||
docsPerPage: 25,
|
||||
indexName: 'indexName',
|
||||
pagination: convertMetaToPagination(INDEX_DOCUMENTS_META_DEFAULT),
|
||||
query: 'test',
|
||||
});
|
||||
jest.useRealTimers();
|
||||
|
@ -84,7 +110,11 @@ describe('DocumentsLogic', () => {
|
|||
expect(mockFlashMessageHelpers.flashAPIErrors).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('clears flash messages on new makeRequest', () => {
|
||||
DocumentsLogic.actions.makeRequest({ indexName: 'index', query: '' });
|
||||
DocumentsLogic.actions.makeRequest({
|
||||
indexName: 'index',
|
||||
pagination: convertMetaToPagination(INDEX_DOCUMENTS_META_DEFAULT),
|
||||
query: '',
|
||||
});
|
||||
expect(mockFlashMessageHelpers.clearFlashMessages).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
@ -99,11 +129,17 @@ describe('DocumentsLogic', () => {
|
|||
};
|
||||
expect(DocumentsLogic.values).toEqual({ ...DEFAULT_VALUES });
|
||||
MappingsApiLogic.actions.apiSuccess({ mappings: {} });
|
||||
SearchDocumentsApiLogic.actions.apiSuccess(mockSuccessData);
|
||||
SearchDocumentsApiLogic.actions.apiSuccess({
|
||||
meta: INDEX_DOCUMENTS_META_DEFAULT,
|
||||
results: mockSuccessData,
|
||||
});
|
||||
|
||||
expect(DocumentsLogic.values).toEqual({
|
||||
...DEFAULT_VALUES,
|
||||
data: mockSuccessData,
|
||||
data: {
|
||||
meta: INDEX_DOCUMENTS_META_DEFAULT,
|
||||
results: mockSuccessData,
|
||||
},
|
||||
isLoading: false,
|
||||
mappingData: {
|
||||
mappings: {},
|
||||
|
@ -135,17 +171,43 @@ describe('DocumentsLogic', () => {
|
|||
};
|
||||
|
||||
MappingsApiLogic.actions.apiSuccess({ mappings: {} });
|
||||
SearchDocumentsApiLogic.actions.apiSuccess(mockSuccessData);
|
||||
SearchDocumentsApiLogic.actions.apiSuccess({
|
||||
meta: {
|
||||
page: {
|
||||
...INDEX_DOCUMENTS_META_DEFAULT.page,
|
||||
total_pages: 1,
|
||||
total_results: 1,
|
||||
},
|
||||
},
|
||||
results: mockSuccessData,
|
||||
});
|
||||
|
||||
expect(DocumentsLogic.values).toEqual({
|
||||
...DEFAULT_VALUES,
|
||||
data: mockSuccessData,
|
||||
data: {
|
||||
meta: {
|
||||
page: {
|
||||
...INDEX_DOCUMENTS_META_DEFAULT.page,
|
||||
total_pages: 1,
|
||||
total_results: 1,
|
||||
},
|
||||
},
|
||||
results: mockSuccessData,
|
||||
},
|
||||
isLoading: false,
|
||||
mappingData: {
|
||||
mappings: {},
|
||||
},
|
||||
mappingStatus: Status.SUCCESS,
|
||||
meta: {
|
||||
page: {
|
||||
...INDEX_DOCUMENTS_META_DEFAULT.page,
|
||||
total_pages: 1,
|
||||
total_results: 1,
|
||||
},
|
||||
},
|
||||
results: [{ _id: '123', _index: 'indexName', searchHit: true }],
|
||||
simplifiedMapping: undefined,
|
||||
status: Status.SUCCESS,
|
||||
});
|
||||
});
|
||||
|
@ -158,11 +220,14 @@ describe('DocumentsLogic', () => {
|
|||
};
|
||||
|
||||
MappingsApiLogic.actions.apiSuccess({ mappings: {} });
|
||||
SearchDocumentsApiLogic.actions.apiSuccess(mockSuccessData);
|
||||
SearchDocumentsApiLogic.actions.apiSuccess({
|
||||
meta: INDEX_DOCUMENTS_META_DEFAULT,
|
||||
results: mockSuccessData,
|
||||
});
|
||||
|
||||
expect(DocumentsLogic.values).toEqual({
|
||||
...DEFAULT_VALUES,
|
||||
data: mockSuccessData,
|
||||
data: { meta: INDEX_DOCUMENTS_META_DEFAULT, results: mockSuccessData },
|
||||
isLoading: false,
|
||||
mappingData: {
|
||||
mappings: {},
|
||||
|
@ -182,7 +247,7 @@ describe('DocumentsLogic', () => {
|
|||
|
||||
expect(DocumentsLogic.values).toEqual({
|
||||
...DEFAULT_VALUES,
|
||||
isLoading: false,
|
||||
isLoading: true,
|
||||
mappingData: {
|
||||
mappings: { properties: { some: { type: 'text' } } },
|
||||
},
|
||||
|
|
|
@ -14,44 +14,74 @@ import {
|
|||
SearchHit,
|
||||
} from '@elastic/elasticsearch/lib/api/types';
|
||||
|
||||
import { ENTERPRISE_SEARCH_DOCUMENTS_DEFAULT_DOC_COUNT } from '../../../../../common/constants';
|
||||
import { Meta } from '../../../../../common/types';
|
||||
import { HttpError, Status } from '../../../../../common/types/api';
|
||||
|
||||
import { flashAPIErrors, clearFlashMessages } from '../../../shared/flash_messages';
|
||||
import { updateMetaPageIndex } from '../../../shared/table_pagination';
|
||||
|
||||
import { MappingsApiLogic } from '../../api/mappings/mappings_logic';
|
||||
import { SearchDocumentsApiLogic } from '../../api/search_documents/search_documents_logic';
|
||||
|
||||
import { IndexNameLogic } from './index_name_logic';
|
||||
|
||||
export const INDEX_DOCUMENTS_META_DEFAULT = {
|
||||
page: {
|
||||
current: 0,
|
||||
size: ENTERPRISE_SEARCH_DOCUMENTS_DEFAULT_DOC_COUNT,
|
||||
total_pages: 0,
|
||||
total_results: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export const DEFAULT_PAGINATION = {
|
||||
pageIndex: INDEX_DOCUMENTS_META_DEFAULT.page.current,
|
||||
pageSize: INDEX_DOCUMENTS_META_DEFAULT.page.size,
|
||||
totalItemCount: INDEX_DOCUMENTS_META_DEFAULT.page.total_results,
|
||||
};
|
||||
|
||||
interface DocumentsLogicActions {
|
||||
apiError(error: HttpError): HttpError;
|
||||
apiReset: typeof SearchDocumentsApiLogic.actions.apiReset;
|
||||
makeMappingRequest: typeof MappingsApiLogic.actions.makeRequest;
|
||||
makeRequest: typeof SearchDocumentsApiLogic.actions.makeRequest;
|
||||
mappingsApiError(error: HttpError): HttpError;
|
||||
onPaginate(newPageIndex: number): { newPageIndex: number };
|
||||
setDocsPerPage(docsPerPage: number): { docsPerPage: number };
|
||||
setSearchQuery(query: string): { query: string };
|
||||
}
|
||||
|
||||
interface DocumentsLogicValues {
|
||||
export interface DocumentsLogicValues {
|
||||
data: typeof SearchDocumentsApiLogic.values.data;
|
||||
docsPerPage: number;
|
||||
indexName: typeof IndexNameLogic.values.indexName;
|
||||
isLoading: boolean;
|
||||
mappingData: IndicesGetMappingIndexMappingRecord;
|
||||
mappingStatus: Status;
|
||||
meta: Meta;
|
||||
query: string;
|
||||
results: SearchHit[];
|
||||
simplifiedMapping: Record<string, MappingProperty> | undefined;
|
||||
status: Status;
|
||||
}
|
||||
|
||||
export const convertMetaToPagination = (meta: Meta) => ({
|
||||
pageIndex: meta.page.current,
|
||||
pageSize: meta.page.size,
|
||||
totalItemCount: meta.page.total_results,
|
||||
});
|
||||
|
||||
export const DocumentsLogic = kea<MakeLogicType<DocumentsLogicValues, DocumentsLogicActions>>({
|
||||
actions: {
|
||||
onPaginate: (newPageIndex) => ({ newPageIndex }),
|
||||
setDocsPerPage: (docsPerPage) => ({ docsPerPage }),
|
||||
setSearchQuery: (query) => ({ query }),
|
||||
},
|
||||
connect: {
|
||||
actions: [
|
||||
SearchDocumentsApiLogic,
|
||||
['apiReset', 'makeRequest', 'apiError'],
|
||||
['apiReset', 'makeRequest', 'apiError', 'apiSuccess'],
|
||||
MappingsApiLogic,
|
||||
['makeRequest as makeMappingRequest', 'apiError as mappingsApiError'],
|
||||
],
|
||||
|
@ -68,13 +98,50 @@ export const DocumentsLogic = kea<MakeLogicType<DocumentsLogicValues, DocumentsL
|
|||
apiError: (e) => flashAPIErrors(e),
|
||||
makeRequest: () => clearFlashMessages(),
|
||||
mappingsApiError: (e) => flashAPIErrors(e),
|
||||
onPaginate: () => {
|
||||
actions.makeRequest({
|
||||
docsPerPage: values.docsPerPage,
|
||||
indexName: values.indexName,
|
||||
pagination: convertMetaToPagination(values.meta),
|
||||
query: values.query,
|
||||
});
|
||||
},
|
||||
setDocsPerPage: () => {
|
||||
actions.makeRequest({
|
||||
docsPerPage: values.docsPerPage,
|
||||
indexName: values.indexName,
|
||||
pagination: convertMetaToPagination(values.meta),
|
||||
query: values.query,
|
||||
});
|
||||
},
|
||||
setSearchQuery: async (_, breakpoint) => {
|
||||
await breakpoint(250);
|
||||
actions.makeRequest({ indexName: values.indexName, query: values.query });
|
||||
actions.makeRequest({
|
||||
docsPerPage: values.docsPerPage,
|
||||
indexName: values.indexName,
|
||||
pagination: convertMetaToPagination(values.meta),
|
||||
query: values.query,
|
||||
});
|
||||
},
|
||||
}),
|
||||
path: ['enterprise_search', 'search_index', 'documents'],
|
||||
reducers: () => ({
|
||||
docsPerPage: [
|
||||
ENTERPRISE_SEARCH_DOCUMENTS_DEFAULT_DOC_COUNT,
|
||||
{
|
||||
setDocsPerPage: (_, { docsPerPage }) => docsPerPage,
|
||||
},
|
||||
],
|
||||
meta: [
|
||||
INDEX_DOCUMENTS_META_DEFAULT,
|
||||
{
|
||||
apiSuccess: (_, { meta }) => meta,
|
||||
onPaginate: (state, { newPageIndex }) => updateMetaPageIndex(state, newPageIndex),
|
||||
setDocsPerPage: (_, { docsPerPage }) => ({
|
||||
page: { ...INDEX_DOCUMENTS_META_DEFAULT.page, size: docsPerPage },
|
||||
}),
|
||||
},
|
||||
],
|
||||
query: [
|
||||
'',
|
||||
{
|
||||
|
@ -85,14 +152,12 @@ export const DocumentsLogic = kea<MakeLogicType<DocumentsLogicValues, DocumentsL
|
|||
selectors: ({ selectors }) => ({
|
||||
isLoading: [
|
||||
() => [selectors.status, selectors.mappingStatus],
|
||||
(status, mappingStatus) => status !== Status.SUCCESS && mappingStatus !== Status.SUCCESS,
|
||||
(status, mappingStatus) => status !== Status.SUCCESS || mappingStatus !== Status.SUCCESS,
|
||||
],
|
||||
results: [
|
||||
() => [selectors.data, selectors.isLoading],
|
||||
(data: SearchResponseBody, isLoading) => {
|
||||
if (isLoading) return [];
|
||||
|
||||
return data?.hits?.hits || [];
|
||||
() => [selectors.data],
|
||||
(data: { results: SearchResponseBody }) => {
|
||||
return data?.results?.hits?.hits || [];
|
||||
},
|
||||
],
|
||||
simplifiedMapping: [
|
||||
|
|
|
@ -8,8 +8,11 @@
|
|||
import { SearchResponseBody } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { IScopedClusterClient } from '@kbn/core/server';
|
||||
|
||||
import { ENTERPRISE_SEARCH_DOCUMENTS_DEFAULT_DOC_COUNT } from '../../common/constants';
|
||||
|
||||
import { fetchSearchResults } from './fetch_search_results';
|
||||
|
||||
const DEFAULT_FROM_VALUE = 0;
|
||||
describe('fetchSearchResults lib function', () => {
|
||||
const mockClient = {
|
||||
asCurrentUser: {
|
||||
|
@ -82,8 +85,26 @@ describe('fetchSearchResults lib function', () => {
|
|||
).resolves.toEqual(regularSearchResultsResponse);
|
||||
|
||||
expect(mockClient.asCurrentUser.search).toHaveBeenCalledWith({
|
||||
from: DEFAULT_FROM_VALUE,
|
||||
index: indexName,
|
||||
q: query,
|
||||
size: ENTERPRISE_SEARCH_DOCUMENTS_DEFAULT_DOC_COUNT,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return search results with hits when no query is passed', async () => {
|
||||
mockClient.asCurrentUser.search.mockImplementation(
|
||||
() => regularSearchResultsResponse as SearchResponseBody
|
||||
);
|
||||
|
||||
await expect(
|
||||
fetchSearchResults(mockClient as unknown as IScopedClusterClient, indexName)
|
||||
).resolves.toEqual(regularSearchResultsResponse);
|
||||
|
||||
expect(mockClient.asCurrentUser.search).toHaveBeenCalledWith({
|
||||
from: DEFAULT_FROM_VALUE,
|
||||
index: indexName,
|
||||
size: ENTERPRISE_SEARCH_DOCUMENTS_DEFAULT_DOC_COUNT,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -97,8 +118,10 @@ describe('fetchSearchResults lib function', () => {
|
|||
).resolves.toEqual(emptySearchResultsResponse);
|
||||
|
||||
expect(mockClient.asCurrentUser.search).toHaveBeenCalledWith({
|
||||
from: DEFAULT_FROM_VALUE,
|
||||
index: indexName,
|
||||
q: query,
|
||||
size: ENTERPRISE_SEARCH_DOCUMENTS_DEFAULT_DOC_COUNT,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,13 +8,19 @@
|
|||
import { SearchResponseBody } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { IScopedClusterClient } from '@kbn/core/server';
|
||||
|
||||
import { ENTERPRISE_SEARCH_DOCUMENTS_DEFAULT_DOC_COUNT } from '../../common/constants';
|
||||
|
||||
export const fetchSearchResults = async (
|
||||
client: IScopedClusterClient,
|
||||
indexName: string,
|
||||
query?: string
|
||||
query?: string,
|
||||
from: number = 0,
|
||||
size: number = ENTERPRISE_SEARCH_DOCUMENTS_DEFAULT_DOC_COUNT
|
||||
): Promise<SearchResponseBody> => {
|
||||
const results = await client.asCurrentUser.search({
|
||||
from,
|
||||
index: indexName,
|
||||
size,
|
||||
...(!!query ? { q: query } : {}),
|
||||
});
|
||||
return results;
|
||||
|
|
|
@ -16,7 +16,7 @@ import { fetchSearchResults } from '../../lib/fetch_search_results';
|
|||
|
||||
import { registerSearchRoute } from './search';
|
||||
|
||||
describe('Elasticsearch Index Mapping', () => {
|
||||
describe('Elasticsearch Search', () => {
|
||||
let mockRouter: MockRouter;
|
||||
const mockClient = {};
|
||||
|
||||
|
@ -76,10 +76,104 @@ describe('Elasticsearch Index Mapping', () => {
|
|||
params: { index_name: 'search-index-name', query: 'banana' },
|
||||
});
|
||||
|
||||
expect(fetchSearchResults).toHaveBeenCalledWith(mockClient, 'search-index-name', 'banana');
|
||||
expect(fetchSearchResults).toHaveBeenCalledWith(
|
||||
mockClient,
|
||||
'search-index-name',
|
||||
'banana',
|
||||
0,
|
||||
25
|
||||
);
|
||||
|
||||
expect(mockRouter.response.ok).toHaveBeenCalledWith({
|
||||
body: mockData,
|
||||
body: {
|
||||
meta: {
|
||||
page: {
|
||||
current: 0,
|
||||
size: 1,
|
||||
total_pages: 1,
|
||||
total_results: 1,
|
||||
},
|
||||
},
|
||||
results: mockData,
|
||||
},
|
||||
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /internal/enterprise_search/indices/{index_name}/search', () => {
|
||||
let mockRouterNoQuery: MockRouter;
|
||||
beforeEach(() => {
|
||||
const context = {
|
||||
core: Promise.resolve({ elasticsearch: { client: mockClient } }),
|
||||
} as jest.Mocked<RequestHandlerContext>;
|
||||
|
||||
mockRouterNoQuery = new MockRouter({
|
||||
context,
|
||||
method: 'get',
|
||||
path: '/internal/enterprise_search/indices/{index_name}/search',
|
||||
});
|
||||
|
||||
registerSearchRoute({
|
||||
...mockDependencies,
|
||||
router: mockRouterNoQuery.router,
|
||||
});
|
||||
});
|
||||
it('fails validation without index_name', () => {
|
||||
const request = { params: {} };
|
||||
mockRouterNoQuery.shouldThrow(request);
|
||||
});
|
||||
|
||||
it('searches returns first 25 search results by default', async () => {
|
||||
const mockData = {
|
||||
_shards: { failed: 0, skipped: 0, successful: 2, total: 2 },
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_id: '5a12292a0f5ae10021650d7e',
|
||||
_index: 'search-regular-index',
|
||||
_score: 4.437291,
|
||||
_source: { id: '5a12292a0f5ae10021650d7e', name: 'banana' },
|
||||
},
|
||||
],
|
||||
|
||||
max_score: null,
|
||||
total: { relation: 'eq', value: 1 },
|
||||
},
|
||||
timed_out: false,
|
||||
took: 4,
|
||||
};
|
||||
|
||||
(fetchSearchResults as jest.Mock).mockImplementationOnce(() => {
|
||||
return Promise.resolve(mockData);
|
||||
});
|
||||
|
||||
await mockRouterNoQuery.callRoute({
|
||||
params: { index_name: 'search-index-name' },
|
||||
});
|
||||
|
||||
expect(fetchSearchResults).toHaveBeenCalledWith(
|
||||
mockClient,
|
||||
'search-index-name',
|
||||
'banana',
|
||||
0,
|
||||
25
|
||||
);
|
||||
|
||||
expect(mockRouterNoQuery.response.ok).toHaveBeenCalledWith({
|
||||
body: {
|
||||
meta: {
|
||||
page: {
|
||||
current: 0,
|
||||
size: 1,
|
||||
total_pages: 1,
|
||||
total_results: 1,
|
||||
},
|
||||
},
|
||||
results: mockData,
|
||||
},
|
||||
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,11 +5,36 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SearchResponseBody } from '@elastic/elasticsearch/lib/api/types';
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
|
||||
import { ENTERPRISE_SEARCH_DOCUMENTS_DEFAULT_DOC_COUNT } from '../../../common/constants';
|
||||
|
||||
import { fetchSearchResults } from '../../lib/fetch_search_results';
|
||||
import { RouteDependencies } from '../../plugin';
|
||||
|
||||
const calculateMeta = (searchResults: SearchResponseBody, page: number, size: number) => {
|
||||
let totalResults = 0;
|
||||
if (searchResults.hits.total === null || searchResults.hits.total === undefined) {
|
||||
totalResults = 0;
|
||||
} else if (typeof searchResults.hits.total === 'number') {
|
||||
totalResults = searchResults.hits.total;
|
||||
} else {
|
||||
totalResults = searchResults.hits.total.value;
|
||||
}
|
||||
const totalPages = Math.ceil(totalResults / size) || 1;
|
||||
|
||||
return {
|
||||
page: {
|
||||
current: page,
|
||||
size: searchResults.hits.hits.length,
|
||||
total_pages: (Number.isFinite(totalPages) && totalPages) || 1,
|
||||
total_results: totalResults,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export function registerSearchRoute({ router }: RouteDependencies) {
|
||||
router.get(
|
||||
{
|
||||
|
@ -18,14 +43,33 @@ export function registerSearchRoute({ router }: RouteDependencies) {
|
|||
params: schema.object({
|
||||
index_name: schema.string(),
|
||||
}),
|
||||
query: schema.object({
|
||||
page: schema.number({ defaultValue: 0, min: 0 }),
|
||||
size: schema.number({
|
||||
defaultValue: ENTERPRISE_SEARCH_DOCUMENTS_DEFAULT_DOC_COUNT,
|
||||
min: 0,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const { client } = (await context.core).elasticsearch;
|
||||
const { page = 0, size = ENTERPRISE_SEARCH_DOCUMENTS_DEFAULT_DOC_COUNT } = request.query;
|
||||
const from = page * size;
|
||||
try {
|
||||
const searchResults = await fetchSearchResults(client, request.params.index_name, '');
|
||||
const searchResults: SearchResponseBody = await fetchSearchResults(
|
||||
client,
|
||||
request.params.index_name,
|
||||
'',
|
||||
from,
|
||||
size
|
||||
);
|
||||
|
||||
return response.ok({
|
||||
body: searchResults,
|
||||
body: {
|
||||
meta: calculateMeta(searchResults, page, size),
|
||||
results: searchResults,
|
||||
},
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
} catch (error) {
|
||||
|
@ -44,18 +88,33 @@ export function registerSearchRoute({ router }: RouteDependencies) {
|
|||
index_name: schema.string(),
|
||||
query: schema.string(),
|
||||
}),
|
||||
query: schema.object({
|
||||
page: schema.number({ defaultValue: 0, min: 0 }),
|
||||
size: schema.number({
|
||||
defaultValue: ENTERPRISE_SEARCH_DOCUMENTS_DEFAULT_DOC_COUNT,
|
||||
min: 0,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
async (context, request, response) => {
|
||||
const { client } = (await context.core).elasticsearch;
|
||||
const { page = 0, size = ENTERPRISE_SEARCH_DOCUMENTS_DEFAULT_DOC_COUNT } = request.query;
|
||||
const from = page * size;
|
||||
try {
|
||||
const searchResults = await fetchSearchResults(
|
||||
client,
|
||||
request.params.index_name,
|
||||
request.params.query
|
||||
request.params.query,
|
||||
from,
|
||||
size
|
||||
);
|
||||
|
||||
return response.ok({
|
||||
body: searchResults,
|
||||
body: {
|
||||
meta: calculateMeta(searchResults, page, size),
|
||||
results: searchResults,
|
||||
},
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue