Skip to content

Commit

Permalink
[Enterprise Search] Add missing pagination to documents (#137998)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
efegurkan authored Aug 3, 2022
1 parent b17579a commit e3760c5
Show file tree
Hide file tree
Showing 11 changed files with 674 additions and 71 deletions.
2 changes: 2 additions & 0 deletions x-pack/plugins/enterprise_search/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
});
Original file line number Diff line number Diff line change
@@ -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>
)}
</>
);
};
Loading

0 comments on commit e3760c5

Please sign in to comment.