mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Enterprise Search] Fix documents pagination (#161830)
## Summary This fixes broken pagination in access control indices, by splitting the fetching logic out and making it unique per index name.
This commit is contained in:
parent
e4ec664ae2
commit
1727a5ed77
10 changed files with 138 additions and 473 deletions
|
@ -7,7 +7,7 @@
|
|||
|
||||
import type { IndicesGetMappingIndexMappingRecord } from '@elastic/elasticsearch/lib/api/types';
|
||||
|
||||
import { createApiLogic } from '../../../shared/api_logic/create_api_logic';
|
||||
import { Actions, createApiLogic } from '../../../shared/api_logic/create_api_logic';
|
||||
import { HttpLogic } from '../../../shared/http';
|
||||
|
||||
export interface GetMappingsArgs {
|
||||
|
@ -16,10 +16,15 @@ export interface GetMappingsArgs {
|
|||
|
||||
export type GetMappingsResponse = IndicesGetMappingIndexMappingRecord;
|
||||
|
||||
export type GetMappingsActions = Actions<GetMappingsArgs, GetMappingsResponse>;
|
||||
|
||||
export const getMappings = async ({ indexName }: GetMappingsArgs) => {
|
||||
const route = `/internal/enterprise_search/mappings/${indexName}`;
|
||||
|
||||
return await HttpLogic.values.http.get<IndicesGetMappingIndexMappingRecord>(route);
|
||||
};
|
||||
|
||||
export const mappingsWithPropsApiLogic = (indexName: string) =>
|
||||
createApiLogic(['mappings_api_logic', indexName], getMappings);
|
||||
|
||||
export const MappingsApiLogic = createApiLogic(['mappings_api_logic'], getMappings);
|
||||
|
|
|
@ -9,20 +9,32 @@ import { SearchResponseBody } from '@elastic/elasticsearch/lib/api/types';
|
|||
|
||||
import { Meta } from '../../../../../common/types';
|
||||
|
||||
import { createApiLogic } from '../../../shared/api_logic/create_api_logic';
|
||||
import { Actions, createApiLogic } from '../../../shared/api_logic/create_api_logic';
|
||||
import { HttpLogic } from '../../../shared/http';
|
||||
|
||||
export interface SearchDocumentsApiLogicArgs {
|
||||
docsPerPage?: number;
|
||||
indexName: string;
|
||||
pagination: { pageIndex: number; pageSize: number };
|
||||
query?: string;
|
||||
}
|
||||
|
||||
export interface SearchDocumentsApiLogicValues {
|
||||
meta: Meta;
|
||||
results: SearchResponseBody;
|
||||
}
|
||||
|
||||
export type SearchDocumentsApiLogicActions = Actions<
|
||||
SearchDocumentsApiLogicArgs,
|
||||
SearchDocumentsApiLogicValues
|
||||
>;
|
||||
|
||||
export const searchDocuments = async ({
|
||||
docsPerPage,
|
||||
indexName,
|
||||
pagination,
|
||||
query: searchQuery,
|
||||
}: {
|
||||
docsPerPage?: number;
|
||||
indexName: string;
|
||||
pagination: { pageIndex: number; pageSize: number };
|
||||
query?: string;
|
||||
}) => {
|
||||
}: SearchDocumentsApiLogicArgs) => {
|
||||
const newIndexName = encodeURIComponent(indexName);
|
||||
const route = `/internal/enterprise_search/indices/${newIndexName}/search`;
|
||||
const query = {
|
||||
|
@ -30,7 +42,7 @@ export const searchDocuments = async ({
|
|||
size: docsPerPage || pagination.pageSize,
|
||||
};
|
||||
|
||||
return await HttpLogic.values.http.post<{ meta: Meta; results: SearchResponseBody }>(route, {
|
||||
return await HttpLogic.values.http.post<SearchDocumentsApiLogicValues>(route, {
|
||||
body: JSON.stringify({
|
||||
searchQuery,
|
||||
}),
|
||||
|
@ -38,7 +50,5 @@ export const searchDocuments = async ({
|
|||
});
|
||||
};
|
||||
|
||||
export const SearchDocumentsApiLogic = createApiLogic(
|
||||
['search_documents_api_logic'],
|
||||
searchDocuments
|
||||
);
|
||||
export const searchDocumentsApiLogic = (indexName: string) =>
|
||||
createApiLogic(['search_documents_api_logic', indexName], searchDocuments);
|
||||
|
|
|
@ -13,26 +13,22 @@ 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 { INDEX_DOCUMENTS_META_DEFAULT } from '../../documents';
|
||||
|
||||
import { DocumentList } from './document_list';
|
||||
|
||||
const mockActions = {};
|
||||
|
||||
export const DEFAULT_VALUES = {
|
||||
data: undefined,
|
||||
indexName: 'indexName',
|
||||
docs: [],
|
||||
docsPerPage: 25,
|
||||
isLoading: true,
|
||||
mappingData: undefined,
|
||||
mappingStatus: 0,
|
||||
mappings: undefined,
|
||||
meta: INDEX_DOCUMENTS_META_DEFAULT,
|
||||
query: '',
|
||||
results: [],
|
||||
status: Status.IDLE,
|
||||
onPaginate: () => {},
|
||||
setDocsPerPage: () => {},
|
||||
};
|
||||
|
||||
const mockValues = { ...DEFAULT_VALUES };
|
||||
|
@ -44,15 +40,15 @@ describe('DocumentList', () => {
|
|||
setMockActions(mockActions);
|
||||
});
|
||||
it('renders empty', () => {
|
||||
const wrapper = shallow(<DocumentList />);
|
||||
const wrapper = shallow(<DocumentList {...DEFAULT_VALUES} />);
|
||||
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: [
|
||||
const values = {
|
||||
...DEFAULT_VALUES,
|
||||
docs: [
|
||||
{
|
||||
_id: 'M9ntXoIBTq5dF-1Xnc8A',
|
||||
_index: 'kibana_sample_data_flights',
|
||||
|
@ -70,19 +66,20 @@ describe('DocumentList', () => {
|
|||
},
|
||||
},
|
||||
],
|
||||
simplifiedMapping: {
|
||||
mappings: {
|
||||
AvgTicketPrice: {
|
||||
type: 'float',
|
||||
type: 'float' as const,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const wrapper = shallow(<DocumentList />);
|
||||
const wrapper = shallow(<DocumentList {...values} />);
|
||||
expect(wrapper.find(Result)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('renders callout when total results are 10.000', () => {
|
||||
setMockValues({
|
||||
const values = {
|
||||
...DEFAULT_VALUES,
|
||||
...mockValues,
|
||||
meta: {
|
||||
page: {
|
||||
|
@ -90,8 +87,8 @@ describe('DocumentList', () => {
|
|||
total_results: 10000,
|
||||
},
|
||||
},
|
||||
});
|
||||
const wrapper = shallow(<DocumentList />);
|
||||
};
|
||||
const wrapper = shallow(<DocumentList {...values} />);
|
||||
expect(wrapper.find(EuiCallOut)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { useActions, useValues } from 'kea';
|
||||
import { useValues } from 'kea';
|
||||
|
||||
import { SearchHit } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { MappingProperty, SearchHit } from '@elastic/elasticsearch/lib/api/types';
|
||||
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
|
@ -29,22 +29,33 @@ import { i18n } from '@kbn/i18n';
|
|||
|
||||
import { FormattedMessage, FormattedNumber } from '@kbn/i18n-react';
|
||||
|
||||
import { Meta } from '../../../../../../../common/types';
|
||||
|
||||
import { Result } from '../../../../../shared/result/result';
|
||||
import { resultMetaData } from '../../../../../shared/result/result_metadata';
|
||||
|
||||
import { DocumentsLogic } from '../../documents_logic';
|
||||
import { IndexViewLogic } from '../../index_view_logic';
|
||||
|
||||
export const DocumentList: React.FC = () => {
|
||||
const {
|
||||
docsPerPage,
|
||||
isLoading,
|
||||
meta,
|
||||
results,
|
||||
simplifiedMapping: mappings,
|
||||
} = useValues(DocumentsLogic);
|
||||
interface DocumentListProps {
|
||||
docs: SearchHit[];
|
||||
docsPerPage: number;
|
||||
isLoading: boolean;
|
||||
mappings: Record<string, MappingProperty> | undefined;
|
||||
meta: Meta;
|
||||
onPaginate: (newPageIndex: number) => void;
|
||||
setDocsPerPage: (docsPerPage: number) => void;
|
||||
}
|
||||
|
||||
export const DocumentList: React.FC<DocumentListProps> = ({
|
||||
docs,
|
||||
docsPerPage,
|
||||
isLoading,
|
||||
mappings,
|
||||
meta,
|
||||
onPaginate,
|
||||
setDocsPerPage,
|
||||
}) => {
|
||||
const { ingestionMethod } = useValues(IndexViewLogic);
|
||||
const { onPaginate, setDocsPerPage } = useActions(DocumentsLogic);
|
||||
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
|
||||
const resultToField = (result: SearchHit) => {
|
||||
|
@ -88,7 +99,7 @@ export const DocumentList: React.FC = () => {
|
|||
maximum: <FormattedNumber value={10000} />,
|
||||
results: (
|
||||
<strong>
|
||||
<FormattedNumber value={results.length} />
|
||||
<FormattedNumber value={docs.length} />
|
||||
</strong>
|
||||
),
|
||||
total: (
|
||||
|
@ -102,10 +113,10 @@ export const DocumentList: React.FC = () => {
|
|||
</EuiText>
|
||||
{isLoading && <EuiProgress size="xs" color="primary" />}
|
||||
<EuiSpacer size="m" />
|
||||
{results.map((result) => {
|
||||
{docs.map((doc) => {
|
||||
return (
|
||||
<React.Fragment key={result._id}>
|
||||
<Result fields={resultToField(result)} metaData={resultMetaData(result)} />
|
||||
<React.Fragment key={doc._id}>
|
||||
<Result fields={resultToField(doc)} metaData={resultMetaData(doc)} />
|
||||
<EuiSpacer size="s" />
|
||||
</React.Fragment>
|
||||
);
|
||||
|
|
|
@ -20,25 +20,46 @@ import {
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { CONNECTORS_ACCESS_CONTROL_INDEX_PREFIX } from '../../../../../common/constants';
|
||||
import {
|
||||
CONNECTORS_ACCESS_CONTROL_INDEX_PREFIX,
|
||||
ENTERPRISE_SEARCH_DOCUMENTS_DEFAULT_DOC_COUNT,
|
||||
} from '../../../../../common/constants';
|
||||
import { Status } from '../../../../../common/types/api';
|
||||
|
||||
import { DEFAULT_META } from '../../../shared/constants';
|
||||
import { KibanaLogic } from '../../../shared/kibana';
|
||||
|
||||
import { mappingsWithPropsApiLogic } from '../../api/mappings/mappings_logic';
|
||||
|
||||
import { searchDocumentsApiLogic } from '../../api/search_documents/search_documents_api_logic';
|
||||
|
||||
import {
|
||||
AccessControlIndexSelector,
|
||||
AccessControlSelectorOption,
|
||||
} from './components/access_control_index_selector/access_control_index_selector';
|
||||
import { DocumentList } from './components/document_list/document_list';
|
||||
import { DocumentsLogic, DEFAULT_PAGINATION } from './documents_logic';
|
||||
import { IndexNameLogic } from './index_name_logic';
|
||||
import { IndexViewLogic } from './index_view_logic';
|
||||
import './documents.scss';
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
export const SearchIndexDocuments: React.FC = () => {
|
||||
const { indexName } = useValues(IndexNameLogic);
|
||||
const { ingestionMethod, hasDocumentLevelSecurityFeature } = useValues(IndexViewLogic);
|
||||
const { simplifiedMapping } = useValues(DocumentsLogic);
|
||||
const { makeRequest, makeMappingRequest, setSearchQuery } = useActions(DocumentsLogic);
|
||||
const { productFeatures } = useValues(KibanaLogic);
|
||||
|
||||
const [selectedIndexType, setSelectedIndexType] =
|
||||
|
@ -47,18 +68,35 @@ export const SearchIndexDocuments: React.FC = () => {
|
|||
selectedIndexType === 'content-index'
|
||||
? indexName
|
||||
: indexName.replace('search-', CONNECTORS_ACCESS_CONTROL_INDEX_PREFIX);
|
||||
const mappingLogic = mappingsWithPropsApiLogic(indexToShow);
|
||||
const documentLogic = searchDocumentsApiLogic(indexToShow);
|
||||
|
||||
const { makeRequest: getDocuments } = useActions(documentLogic);
|
||||
const { makeRequest: getMappings } = useActions(mappingLogic);
|
||||
const { data, status } = useValues(documentLogic);
|
||||
const { data: mappingData, status: mappingStatus } = useValues(mappingLogic);
|
||||
|
||||
const docs = data?.results?.hits.hits ?? [];
|
||||
|
||||
const [pagination, setPagination] = useState(DEFAULT_PAGINATION);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const shouldShowAccessControlSwitcher =
|
||||
hasDocumentLevelSecurityFeature && productFeatures.hasDocumentLevelSecurityEnabled;
|
||||
|
||||
useEffect(() => {
|
||||
makeRequest({
|
||||
getDocuments({
|
||||
indexName: indexToShow,
|
||||
pagination: DEFAULT_PAGINATION,
|
||||
query: '',
|
||||
pagination,
|
||||
query: searchQuery,
|
||||
});
|
||||
makeMappingRequest({ indexName: indexToShow });
|
||||
}, [indexToShow, indexName]);
|
||||
}, [indexToShow, pagination, searchQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
setSearchQuery('');
|
||||
setPagination(DEFAULT_PAGINATION);
|
||||
getMappings({ indexName: indexToShow });
|
||||
}, [indexToShow]);
|
||||
|
||||
return (
|
||||
<EuiPanel hasBorder={false} hasShadow={false} paddingSize="none">
|
||||
|
@ -102,11 +140,21 @@ export const SearchIndexDocuments: React.FC = () => {
|
|||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
{!simplifiedMapping &&
|
||||
{docs.length === 0 &&
|
||||
i18n.translate('xpack.enterpriseSearch.content.searchIndex.documents.noMappings', {
|
||||
defaultMessage: 'No documents found for index',
|
||||
})}
|
||||
{simplifiedMapping && <DocumentList />}
|
||||
{docs.length > 0 && (
|
||||
<DocumentList
|
||||
docs={docs}
|
||||
docsPerPage={pagination.pageSize}
|
||||
isLoading={status !== Status.SUCCESS && mappingStatus !== Status.SUCCESS}
|
||||
mappings={mappingData?.mappings?.properties ?? {}}
|
||||
meta={data?.meta ?? DEFAULT_META}
|
||||
onPaginate={(pageIndex) => setPagination({ ...pagination, pageIndex })}
|
||||
setDocsPerPage={(pageSize) => setPagination({ ...pagination, pageSize })}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
|
|
|
@ -1,243 +0,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 { LogicMounter } from '../../../__mocks__/kea_logic';
|
||||
|
||||
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_api_logic';
|
||||
|
||||
import {
|
||||
DocumentsLogic,
|
||||
INDEX_DOCUMENTS_META_DEFAULT,
|
||||
convertMetaToPagination,
|
||||
} from './documents_logic';
|
||||
import { IndexNameLogic } from './index_name_logic';
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
describe('DocumentsLogic', () => {
|
||||
const { mount: mountIndexNameLogic } = new LogicMounter(IndexNameLogic);
|
||||
const { mount: mountSearchDocumentsApiLogic } = new LogicMounter(SearchDocumentsApiLogic);
|
||||
const { mount: mountMappingsApiLogic } = new LogicMounter(MappingsApiLogic);
|
||||
const { mount } = new LogicMounter(DocumentsLogic);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
const indexNameLogic = mountIndexNameLogic();
|
||||
mountMappingsApiLogic();
|
||||
mountSearchDocumentsApiLogic();
|
||||
mount();
|
||||
indexNameLogic.actions.setIndexName('indexName');
|
||||
});
|
||||
|
||||
it('has expected default values', () => {
|
||||
expect(DocumentsLogic.values).toEqual(DEFAULT_VALUES);
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
describe('setSearchQuery', () => {
|
||||
it('sets query string', () => {
|
||||
const newQueryString = 'foo';
|
||||
expect(DocumentsLogic.values).toEqual({ ...DEFAULT_VALUES });
|
||||
DocumentsLogic.actions.setSearchQuery(newQueryString);
|
||||
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', () => {
|
||||
it('make documents apiRequest request after 250ms debounce', async () => {
|
||||
jest.useFakeTimers({ legacyFakeTimers: true });
|
||||
DocumentsLogic.actions.makeRequest = jest.fn();
|
||||
DocumentsLogic.actions.setSearchQuery('test');
|
||||
await nextTick();
|
||||
expect(DocumentsLogic.actions.makeRequest).not.toHaveBeenCalled();
|
||||
jest.advanceTimersByTime(250);
|
||||
await nextTick();
|
||||
expect(DocumentsLogic.actions.makeRequest).toHaveBeenCalledWith({
|
||||
docsPerPage: 25,
|
||||
indexName: 'indexName',
|
||||
pagination: convertMetaToPagination(INDEX_DOCUMENTS_META_DEFAULT),
|
||||
query: 'test',
|
||||
});
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('selectors', () => {
|
||||
describe('isLoading', () => {
|
||||
it('sets isLoading false when mapping and documents requests are success', () => {
|
||||
const mockSuccessData = {
|
||||
_shards: { failed: 0, successful: 3, total: 3 },
|
||||
hits: { hits: [] },
|
||||
timed_out: false,
|
||||
took: 3,
|
||||
};
|
||||
expect(DocumentsLogic.values).toEqual({ ...DEFAULT_VALUES });
|
||||
MappingsApiLogic.actions.apiSuccess({ mappings: {} });
|
||||
SearchDocumentsApiLogic.actions.apiSuccess({
|
||||
meta: INDEX_DOCUMENTS_META_DEFAULT,
|
||||
results: mockSuccessData,
|
||||
});
|
||||
|
||||
expect(DocumentsLogic.values).toEqual({
|
||||
...DEFAULT_VALUES,
|
||||
data: {
|
||||
meta: INDEX_DOCUMENTS_META_DEFAULT,
|
||||
results: mockSuccessData,
|
||||
},
|
||||
isLoading: false,
|
||||
mappingData: {
|
||||
mappings: {},
|
||||
},
|
||||
mappingStatus: Status.SUCCESS,
|
||||
status: Status.SUCCESS,
|
||||
});
|
||||
});
|
||||
|
||||
it('sets isLoading false when one of mappings or documents requests are not done', () => {
|
||||
expect(DocumentsLogic.values).toEqual({ ...DEFAULT_VALUES });
|
||||
MappingsApiLogic.actions.apiError({} as HttpError);
|
||||
expect(DocumentsLogic.values).toEqual({
|
||||
...DEFAULT_VALUES,
|
||||
isLoading: true,
|
||||
mappingStatus: Status.ERROR,
|
||||
status: Status.IDLE,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('results', () => {
|
||||
it('selects searchHits from the response body', () => {
|
||||
const mockSuccessData = {
|
||||
_shards: { failed: 0, successful: 3, total: 3 },
|
||||
hits: { hits: [{ _id: '123', _index: 'indexName', searchHit: true }] },
|
||||
timed_out: false,
|
||||
took: 3,
|
||||
};
|
||||
|
||||
MappingsApiLogic.actions.apiSuccess({ mappings: {} });
|
||||
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: {
|
||||
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,
|
||||
});
|
||||
});
|
||||
it("returns empty when response doesn't have hits", () => {
|
||||
const mockSuccessData = {
|
||||
_shards: { failed: 0, successful: 3, total: 3 },
|
||||
hits: { hits: [] },
|
||||
timed_out: false,
|
||||
took: 3,
|
||||
};
|
||||
|
||||
MappingsApiLogic.actions.apiSuccess({ mappings: {} });
|
||||
SearchDocumentsApiLogic.actions.apiSuccess({
|
||||
meta: INDEX_DOCUMENTS_META_DEFAULT,
|
||||
results: mockSuccessData,
|
||||
});
|
||||
|
||||
expect(DocumentsLogic.values).toEqual({
|
||||
...DEFAULT_VALUES,
|
||||
data: { meta: INDEX_DOCUMENTS_META_DEFAULT, results: mockSuccessData },
|
||||
isLoading: false,
|
||||
mappingData: {
|
||||
mappings: {},
|
||||
},
|
||||
mappingStatus: Status.SUCCESS,
|
||||
results: [],
|
||||
status: Status.SUCCESS,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('simplifiedMapping', () => {
|
||||
it('selects properties from the response body', () => {
|
||||
MappingsApiLogic.actions.apiSuccess({
|
||||
mappings: { properties: { some: { type: 'text' } } },
|
||||
});
|
||||
|
||||
expect(DocumentsLogic.values).toEqual({
|
||||
...DEFAULT_VALUES,
|
||||
isLoading: true,
|
||||
mappingData: {
|
||||
mappings: { properties: { some: { type: 'text' } } },
|
||||
},
|
||||
mappingStatus: Status.SUCCESS,
|
||||
simplifiedMapping: { some: { type: 'text' } },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,165 +0,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 { kea, MakeLogicType } from 'kea';
|
||||
|
||||
import {
|
||||
IndicesGetMappingIndexMappingRecord,
|
||||
MappingProperty,
|
||||
SearchResponseBody,
|
||||
SearchHit,
|
||||
} from '@elastic/elasticsearch/lib/api/types';
|
||||
|
||||
import { ENTERPRISE_SEARCH_DOCUMENTS_DEFAULT_DOC_COUNT } from '../../../../../common/constants';
|
||||
import { Meta } from '../../../../../common/types';
|
||||
import { Status } from '../../../../../common/types/api';
|
||||
|
||||
import { updateMetaPageIndex } from '../../../shared/table_pagination';
|
||||
|
||||
import { MappingsApiLogic } from '../../api/mappings/mappings_logic';
|
||||
import { SearchDocumentsApiLogic } from '../../api/search_documents/search_documents_api_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 {
|
||||
apiReset: typeof SearchDocumentsApiLogic.actions.apiReset;
|
||||
makeMappingRequest: typeof MappingsApiLogic.actions.makeRequest;
|
||||
makeRequest: typeof SearchDocumentsApiLogic.actions.makeRequest;
|
||||
onPaginate(newPageIndex: number): { newPageIndex: number };
|
||||
setDocsPerPage(docsPerPage: number): { docsPerPage: number };
|
||||
setSearchQuery(query: string): { query: string };
|
||||
}
|
||||
|
||||
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', 'apiSuccess', 'makeRequest'],
|
||||
MappingsApiLogic,
|
||||
['makeRequest as makeMappingRequest'],
|
||||
],
|
||||
values: [
|
||||
SearchDocumentsApiLogic,
|
||||
['data', 'status'],
|
||||
MappingsApiLogic,
|
||||
['data as mappingData', 'status as mappingStatus'],
|
||||
IndexNameLogic,
|
||||
['indexName'],
|
||||
],
|
||||
},
|
||||
listeners: ({ actions, values }) => ({
|
||||
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({
|
||||
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: [
|
||||
'',
|
||||
{
|
||||
setSearchQuery: (_, { query }) => query,
|
||||
},
|
||||
],
|
||||
}),
|
||||
selectors: ({ selectors }) => ({
|
||||
isLoading: [
|
||||
() => [selectors.status, selectors.mappingStatus],
|
||||
(status, mappingStatus) => status !== Status.SUCCESS || mappingStatus !== Status.SUCCESS,
|
||||
],
|
||||
results: [
|
||||
() => [selectors.data],
|
||||
(data: { results: SearchResponseBody }) => {
|
||||
return data?.results?.hits?.hits || [];
|
||||
},
|
||||
],
|
||||
simplifiedMapping: [
|
||||
() => [selectors.mappingStatus, selectors.mappingData],
|
||||
(status: Status, mapping: IndicesGetMappingIndexMappingRecord) => {
|
||||
if (status !== Status.SUCCESS) return;
|
||||
return mapping?.mappings?.properties;
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
|
@ -31,11 +31,12 @@ import { docLinks } from '../../../shared/doc_links';
|
|||
|
||||
import { KibanaLogic } from '../../../shared/kibana';
|
||||
|
||||
import { mappingsWithPropsApiLogic } from '../../api/mappings/mappings_logic';
|
||||
|
||||
import {
|
||||
AccessControlIndexSelector,
|
||||
AccessControlSelectorOption,
|
||||
} from './components/access_control_index_selector/access_control_index_selector';
|
||||
import { DocumentsLogic } from './documents_logic';
|
||||
import { IndexNameLogic } from './index_name_logic';
|
||||
import { IndexViewLogic } from './index_view_logic';
|
||||
|
||||
|
@ -43,8 +44,6 @@ import './index_mappings.scss';
|
|||
|
||||
export const SearchIndexIndexMappings: React.FC = () => {
|
||||
const { indexName } = useValues(IndexNameLogic);
|
||||
const { makeMappingRequest } = useActions(DocumentsLogic);
|
||||
const { mappingData } = useValues(DocumentsLogic);
|
||||
const { hasDocumentLevelSecurityFeature } = useValues(IndexViewLogic);
|
||||
const { productFeatures } = useValues(KibanaLogic);
|
||||
|
||||
|
@ -54,7 +53,8 @@ export const SearchIndexIndexMappings: React.FC = () => {
|
|||
selectedIndexType === 'content-index'
|
||||
? indexName
|
||||
: indexName.replace('search-', CONNECTORS_ACCESS_CONTROL_INDEX_PREFIX);
|
||||
|
||||
const { makeRequest: makeMappingRequest } = useActions(mappingsWithPropsApiLogic(indexToShow));
|
||||
const { data: mappingData } = useValues(mappingsWithPropsApiLogic(indexToShow));
|
||||
const shouldShowAccessControlSwitch =
|
||||
hasDocumentLevelSecurityFeature && productFeatures.hasDocumentLevelSecurityEnabled;
|
||||
useEffect(() => {
|
||||
|
|
|
@ -26,6 +26,7 @@ export interface Actions<Args, Result> {
|
|||
|
||||
export interface CreateApiOptions<Result> {
|
||||
clearFlashMessagesOnMakeRequest: boolean;
|
||||
key: string; // for logics that use a key as a prop
|
||||
requestBreakpointMS?: number;
|
||||
showErrorFlash: boolean;
|
||||
showSuccessFlashFn?: (result: Result) => string;
|
||||
|
|
|
@ -27,6 +27,7 @@ export const ResultFields: React.FC<Props> = ({ fields, isExpanded }) => {
|
|||
fieldName={field.fieldName}
|
||||
fieldValue={field.fieldValue}
|
||||
fieldType={field.fieldType}
|
||||
key={field.fieldName}
|
||||
/>
|
||||
))}
|
||||
</EuiTableBody>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue