Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[8.4] [Enterprise Search] Add missing pagination to documents (#137998) #138010

Merged
merged 1 commit into from
Aug 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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