mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Content Management] Cross Type Search - paginate and tags filter (#154819)
## Summary Follow up to https://github.com/elastic/kibana/pull/154464 Partially resolve https://github.com/elastic/kibana/issues/152224 - implement pagination and tags filtering
This commit is contained in:
parent
3840136d75
commit
10f2015d17
5 changed files with 171 additions and 5 deletions
|
@ -13,6 +13,9 @@ import { createMockedStorage } from './mocks';
|
|||
import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks';
|
||||
import { StorageContext } from '.';
|
||||
|
||||
const SEARCH_LISTING_LIMIT = 100;
|
||||
const SEARCH_PER_PAGE = 10;
|
||||
|
||||
const setup = () => {
|
||||
const contentRegistry = new ContentRegistry(new EventBus());
|
||||
|
||||
|
@ -50,6 +53,10 @@ const setup = () => {
|
|||
const mSearchService = new MSearchService({
|
||||
getSavedObjectsClient: async () => savedObjectsClient,
|
||||
contentRegistry,
|
||||
getConfig: {
|
||||
listingLimit: async () => SEARCH_LISTING_LIMIT,
|
||||
perPage: async () => SEARCH_PER_PAGE,
|
||||
},
|
||||
});
|
||||
|
||||
return { mSearchService, savedObjectsClient, contentRegistry };
|
||||
|
@ -94,7 +101,7 @@ test('should cross-content search using saved objects api', async () => {
|
|||
saved_objects: [soResultFoo, soResultBar],
|
||||
total: 2,
|
||||
page: 1,
|
||||
per_page: 10,
|
||||
per_page: SEARCH_PER_PAGE,
|
||||
});
|
||||
|
||||
const result = await mSearchService.search(
|
||||
|
@ -104,6 +111,10 @@ test('should cross-content search using saved objects api', async () => {
|
|||
],
|
||||
{
|
||||
text: 'search text',
|
||||
tags: {
|
||||
excluded: ['excluded-tag'],
|
||||
included: ['included-tag'],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -112,6 +123,20 @@ test('should cross-content search using saved objects api', async () => {
|
|||
search: 'search text',
|
||||
searchFields: ['title^3', 'description', 'special-foo-field', 'special-bar-field'],
|
||||
type: ['foo-type', 'bar-type'],
|
||||
page: 1,
|
||||
perPage: SEARCH_PER_PAGE,
|
||||
hasNoReference: [
|
||||
{
|
||||
id: 'excluded-tag',
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
hasReference: [
|
||||
{
|
||||
id: 'included-tag',
|
||||
type: 'tag',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
|
@ -161,3 +186,98 @@ test('should error if content is registered, but no mSearch support', async () =
|
|||
)
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"Content type foo2 does not support mSearch"`);
|
||||
});
|
||||
|
||||
test('should paginate using cursor', async () => {
|
||||
const { savedObjectsClient, mSearchService } = setup();
|
||||
|
||||
const soResultFoo = {
|
||||
id: 'fooid',
|
||||
score: 0,
|
||||
type: 'foo-type',
|
||||
references: [],
|
||||
attributes: {
|
||||
title: 'foo',
|
||||
},
|
||||
};
|
||||
|
||||
savedObjectsClient.find.mockResolvedValueOnce({
|
||||
saved_objects: Array(5).fill(soResultFoo),
|
||||
total: 7,
|
||||
page: 1,
|
||||
per_page: 5,
|
||||
});
|
||||
|
||||
const result1 = await mSearchService.search(
|
||||
[
|
||||
{ contentTypeId: 'foo', ctx: mockStorageContext() },
|
||||
{ contentTypeId: 'bar', ctx: mockStorageContext() },
|
||||
],
|
||||
{
|
||||
text: 'search text',
|
||||
limit: 5,
|
||||
}
|
||||
);
|
||||
|
||||
expect(savedObjectsClient.find).toHaveBeenCalledWith({
|
||||
defaultSearchOperator: 'AND',
|
||||
search: 'search text',
|
||||
searchFields: ['title^3', 'description', 'special-foo-field', 'special-bar-field'],
|
||||
type: ['foo-type', 'bar-type'],
|
||||
page: 1,
|
||||
perPage: 5,
|
||||
});
|
||||
|
||||
expect(result1).toEqual({
|
||||
hits: Array(5).fill({ itemFoo: soResultFoo }),
|
||||
pagination: {
|
||||
total: 7,
|
||||
cursor: '2',
|
||||
},
|
||||
});
|
||||
|
||||
savedObjectsClient.find.mockResolvedValueOnce({
|
||||
saved_objects: Array(2).fill(soResultFoo),
|
||||
total: 7,
|
||||
page: 2,
|
||||
per_page: 5,
|
||||
});
|
||||
|
||||
const result2 = await mSearchService.search(
|
||||
[
|
||||
{ contentTypeId: 'foo', ctx: mockStorageContext() },
|
||||
{ contentTypeId: 'bar', ctx: mockStorageContext() },
|
||||
],
|
||||
{
|
||||
text: 'search text',
|
||||
limit: 5,
|
||||
cursor: result1.pagination.cursor,
|
||||
}
|
||||
);
|
||||
|
||||
expect(result2).toEqual({
|
||||
hits: Array(2).fill({ itemFoo: soResultFoo }),
|
||||
pagination: {
|
||||
total: 7,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should error if outside of pagination limit', async () => {
|
||||
const { mSearchService } = setup();
|
||||
await expect(
|
||||
mSearchService.search(
|
||||
[
|
||||
{ contentTypeId: 'foo', ctx: mockStorageContext() },
|
||||
{ contentTypeId: 'bar', ctx: mockStorageContext() },
|
||||
],
|
||||
|
||||
{
|
||||
text: 'search text',
|
||||
cursor: '11',
|
||||
limit: 10,
|
||||
}
|
||||
)
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Requested page 11 with 10 items per page exceeds the maximum allowed limit of ${SEARCH_LISTING_LIMIT} items"`
|
||||
);
|
||||
});
|
||||
|
|
|
@ -6,7 +6,10 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
|
||||
import {
|
||||
SavedObjectsClientContract,
|
||||
SavedObjectsFindOptionsReference,
|
||||
} from '@kbn/core-saved-objects-api-server';
|
||||
import type { MSearchResult, SearchQuery } from '../../common';
|
||||
import { ContentRegistry } from './registry';
|
||||
import { StorageContext } from './types';
|
||||
|
@ -16,6 +19,10 @@ export class MSearchService {
|
|||
private readonly deps: {
|
||||
getSavedObjectsClient: () => Promise<SavedObjectsClientContract>;
|
||||
contentRegistry: ContentRegistry;
|
||||
getConfig: {
|
||||
listingLimit: () => Promise<number>;
|
||||
perPage: () => Promise<number>;
|
||||
};
|
||||
}
|
||||
) {}
|
||||
|
||||
|
@ -56,14 +63,36 @@ export class MSearchService {
|
|||
});
|
||||
|
||||
const savedObjectsClient = await this.deps.getSavedObjectsClient();
|
||||
const listingLimit = await this.deps.getConfig.listingLimit();
|
||||
const defaultPerPage = await this.deps.getConfig.perPage();
|
||||
|
||||
const page = query.cursor ? Number(query.cursor) : 1;
|
||||
const perPage = query.limit ? query.limit : defaultPerPage;
|
||||
|
||||
if (page * perPage > listingLimit) {
|
||||
throw new Error(
|
||||
`Requested page ${page} with ${perPage} items per page exceeds the maximum allowed limit of ${listingLimit} items`
|
||||
);
|
||||
}
|
||||
|
||||
const tagIdToSavedObjectReference = (tagId: string): SavedObjectsFindOptionsReference => ({
|
||||
type: 'tag',
|
||||
id: tagId,
|
||||
});
|
||||
|
||||
const soResult = await savedObjectsClient.find({
|
||||
type: soSearchTypes,
|
||||
|
||||
search: query.text,
|
||||
searchFields: [`title^3`, `description`, ...additionalSearchFields],
|
||||
defaultSearchOperator: 'AND',
|
||||
// TODO: tags
|
||||
// TODO: pagination
|
||||
// TODO: sort
|
||||
|
||||
page,
|
||||
perPage,
|
||||
|
||||
// tags
|
||||
hasReference: query.tags?.included?.map(tagIdToSavedObjectReference),
|
||||
hasNoReference: query.tags?.excluded?.map(tagIdToSavedObjectReference),
|
||||
});
|
||||
|
||||
const contentItemHits = soResult.saved_objects.map((savedObject) => {
|
||||
|
@ -78,6 +107,11 @@ export class MSearchService {
|
|||
hits: contentItemHits,
|
||||
pagination: {
|
||||
total: soResult.total,
|
||||
cursor:
|
||||
soResult.page * soResult.per_page < soResult.total &&
|
||||
(soResult.page + 1) * soResult.per_page < listingLimit
|
||||
? String(soResult.page + 1)
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -123,6 +123,10 @@ describe('RPC -> mSearch()', () => {
|
|||
const mSearchService = new MSearchService({
|
||||
getSavedObjectsClient: async () => savedObjectsClient,
|
||||
contentRegistry,
|
||||
getConfig: {
|
||||
listingLimit: async () => 100,
|
||||
perPage: async () => 10,
|
||||
},
|
||||
});
|
||||
|
||||
const mSearchSpy = jest.spyOn(mSearchService, 'search');
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { schema } from '@kbn/config-schema';
|
||||
import type { IRouter } from '@kbn/core/server';
|
||||
|
||||
import { LISTING_LIMIT_SETTING, PER_PAGE_SETTING } from '@kbn/saved-objects-finder-plugin/common';
|
||||
import { ProcedureName } from '../../../common';
|
||||
import type { ContentRegistry } from '../../core';
|
||||
import { MSearchService } from '../../core/msearch';
|
||||
|
@ -60,6 +61,12 @@ export function initRpcRoutes(
|
|||
getSavedObjectsClient: async () =>
|
||||
(await requestHandlerContext.core).savedObjects.client,
|
||||
contentRegistry,
|
||||
getConfig: {
|
||||
listingLimit: async () =>
|
||||
(await requestHandlerContext.core).uiSettings.client.get(LISTING_LIMIT_SETTING),
|
||||
perPage: async () =>
|
||||
(await requestHandlerContext.core).uiSettings.client.get(PER_PAGE_SETTING),
|
||||
},
|
||||
}),
|
||||
};
|
||||
const { name } = request.params as { name: ProcedureName };
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
"@kbn/object-versioning",
|
||||
"@kbn/core-saved-objects-api-server-mocks",
|
||||
"@kbn/core-saved-objects-api-server",
|
||||
"@kbn/saved-objects-finder-plugin",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue