Skip to content

Commit 04aaba8

Browse files
authored
[GS] add savedObjects result provider (#68619)
* create server-side skeleton * add base implementation & tests * add unit test for provider * remove useless contracts * add preference search option * implement score from find results * fix types * add FTR test * fix test plugin types * address ome review comments * add multi results test * use `getVisibleTypes`
1 parent 2a43b48 commit 04aaba8

File tree

18 files changed

+1382
-7
lines changed

18 files changed

+1382
-7
lines changed

x-pack/plugins/global_search/server/mocks.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
RouteHandlerGlobalSearchContext,
1212
} from './types';
1313
import { searchServiceMock } from './services/search_service.mock';
14+
import { contextMock } from './services/context.mock';
1415

1516
const createSetupMock = (): jest.Mocked<GlobalSearchPluginSetup> => {
1617
const searchMock = searchServiceMock.createSetupContract();
@@ -29,17 +30,18 @@ const createStartMock = (): jest.Mocked<GlobalSearchPluginStart> => {
2930
};
3031

3132
const createRouteHandlerContextMock = (): jest.Mocked<RouteHandlerGlobalSearchContext> => {
32-
const contextMock = {
33+
const handlerContextMock = {
3334
find: jest.fn(),
3435
};
3536

36-
contextMock.find.mockReturnValue(of([]));
37+
handlerContextMock.find.mockReturnValue(of([]));
3738

38-
return contextMock;
39+
return handlerContextMock;
3940
};
4041

4142
export const globalSearchPluginMock = {
4243
createSetupContract: createSetupMock,
4344
createStartContract: createStartMock,
4445
createRouteHandlerContext: createRouteHandlerContextMock,
46+
createProviderContext: contextMock.create,
4547
};
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import {
8+
savedObjectsTypeRegistryMock,
9+
savedObjectsClientMock,
10+
elasticsearchServiceMock,
11+
uiSettingsServiceMock,
12+
} from '../../../../../src/core/server/mocks';
13+
14+
const createContextMock = () => {
15+
return {
16+
core: {
17+
savedObjects: {
18+
client: savedObjectsClientMock.create(),
19+
typeRegistry: savedObjectsTypeRegistryMock.create(),
20+
},
21+
elasticsearch: {
22+
legacy: {
23+
client: elasticsearchServiceMock.createScopedClusterClient(),
24+
},
25+
},
26+
uiSettings: {
27+
client: uiSettingsServiceMock.createClient(),
28+
},
29+
},
30+
};
31+
};
32+
33+
const createFactoryMock = () => () => () => createContextMock();
34+
35+
export const contextMock = {
36+
create: createContextMock,
37+
createFactory: createFactoryMock,
38+
};

x-pack/plugins/global_search_providers/kibana.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"id": "globalSearchProviders",
33
"version": "8.0.0",
44
"kibanaVersion": "kibana",
5-
"server": false,
5+
"server": true,
66
"ui": true,
77
"requiredPlugins": ["globalSearch"],
88
"optionalPlugins": [],
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { PluginInitializer } from 'src/core/server';
8+
import { GlobalSearchProvidersPlugin, GlobalSearchProvidersPluginSetupDeps } from './plugin';
9+
10+
export const plugin: PluginInitializer<{}, {}, GlobalSearchProvidersPluginSetupDeps, {}> = () =>
11+
new GlobalSearchProvidersPlugin();
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { coreMock } from '../../../../src/core/server/mocks';
8+
import { globalSearchPluginMock } from '../../global_search/server/mocks';
9+
import { GlobalSearchProvidersPlugin } from './plugin';
10+
11+
describe('GlobalSearchProvidersPlugin', () => {
12+
let plugin: GlobalSearchProvidersPlugin;
13+
let globalSearchSetup: ReturnType<typeof globalSearchPluginMock.createSetupContract>;
14+
15+
beforeEach(() => {
16+
plugin = new GlobalSearchProvidersPlugin();
17+
globalSearchSetup = globalSearchPluginMock.createSetupContract();
18+
});
19+
20+
describe('#setup', () => {
21+
it('registers the `savedObjects` result provider', () => {
22+
const coreSetup = coreMock.createSetup();
23+
plugin.setup(coreSetup, { globalSearch: globalSearchSetup });
24+
25+
expect(globalSearchSetup.registerResultProvider).toHaveBeenCalledTimes(1);
26+
expect(globalSearchSetup.registerResultProvider).toHaveBeenCalledWith(
27+
expect.objectContaining({
28+
id: 'savedObjects',
29+
})
30+
);
31+
});
32+
});
33+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { CoreSetup, Plugin } from 'src/core/server';
8+
import { GlobalSearchPluginSetup } from '../../global_search/server';
9+
import { createSavedObjectsResultProvider } from './providers';
10+
11+
export interface GlobalSearchProvidersPluginSetupDeps {
12+
globalSearch: GlobalSearchPluginSetup;
13+
}
14+
15+
export class GlobalSearchProvidersPlugin
16+
implements Plugin<{}, {}, GlobalSearchProvidersPluginSetupDeps, {}> {
17+
setup(
18+
{ getStartServices }: CoreSetup<{}, {}>,
19+
{ globalSearch }: GlobalSearchProvidersPluginSetupDeps
20+
) {
21+
globalSearch.registerResultProvider(createSavedObjectsResultProvider());
22+
return {};
23+
}
24+
25+
start() {
26+
return {};
27+
}
28+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
export { createSavedObjectsResultProvider } from './saved_objects';
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
export { createSavedObjectsResultProvider } from './provider';
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { SavedObjectsFindResult, SavedObjectsType, SavedObjectTypeRegistry } from 'src/core/server';
8+
import { mapToResult, mapToResults } from './map_object_to_result';
9+
10+
const createType = (props: Partial<SavedObjectsType>): SavedObjectsType => {
11+
return {
12+
name: 'type',
13+
hidden: false,
14+
namespaceType: 'single',
15+
mappings: { properties: {} },
16+
...props,
17+
};
18+
};
19+
20+
const createObject = <T>(
21+
props: Partial<SavedObjectsFindResult>,
22+
attributes: T
23+
): SavedObjectsFindResult<T> => {
24+
return {
25+
id: 'id',
26+
type: 'dashboard',
27+
references: [],
28+
score: 100,
29+
...props,
30+
attributes,
31+
};
32+
};
33+
34+
describe('mapToResult', () => {
35+
it('converts a savedObject to a result', () => {
36+
const type = createType({
37+
name: 'dashboard',
38+
management: {
39+
defaultSearchField: 'title',
40+
getInAppUrl: (obj) => ({ path: `/dashboard/${obj.id}`, uiCapabilitiesPath: '' }),
41+
},
42+
});
43+
44+
const obj = createObject(
45+
{
46+
id: 'dash1',
47+
type: 'dashboard',
48+
score: 42,
49+
},
50+
{
51+
title: 'My dashboard',
52+
}
53+
);
54+
55+
expect(mapToResult(obj, type)).toEqual({
56+
id: 'dash1',
57+
title: 'My dashboard',
58+
type: 'dashboard',
59+
url: '/dashboard/dash1',
60+
score: 42,
61+
});
62+
});
63+
64+
it('throws if the type do not have management information', () => {
65+
const object = createObject(
66+
{ id: 'dash1', type: 'dashboard', score: 42 },
67+
{ title: 'My dashboard' }
68+
);
69+
70+
expect(() => {
71+
mapToResult(
72+
object,
73+
createType({
74+
name: 'dashboard',
75+
management: {
76+
getInAppUrl: (obj) => ({ path: `/dashboard/${obj.id}`, uiCapabilitiesPath: '' }),
77+
},
78+
})
79+
);
80+
}).toThrowErrorMatchingInlineSnapshot(
81+
`"Trying to map an object from a type without management metadata"`
82+
);
83+
84+
expect(() => {
85+
mapToResult(
86+
object,
87+
createType({
88+
name: 'dashboard',
89+
management: {
90+
defaultSearchField: 'title',
91+
},
92+
})
93+
);
94+
}).toThrowErrorMatchingInlineSnapshot(
95+
`"Trying to map an object from a type without management metadata"`
96+
);
97+
98+
expect(() => {
99+
mapToResult(
100+
object,
101+
createType({
102+
name: 'dashboard',
103+
management: undefined,
104+
})
105+
);
106+
}).toThrowErrorMatchingInlineSnapshot(
107+
`"Trying to map an object from a type without management metadata"`
108+
);
109+
});
110+
});
111+
112+
describe('mapToResults', () => {
113+
let typeRegistry: SavedObjectTypeRegistry;
114+
115+
beforeEach(() => {
116+
typeRegistry = new SavedObjectTypeRegistry();
117+
});
118+
119+
it('converts savedObjects to results', () => {
120+
typeRegistry.registerType(
121+
createType({
122+
name: 'typeA',
123+
management: {
124+
defaultSearchField: 'title',
125+
getInAppUrl: (obj) => ({ path: `/type-a/${obj.id}`, uiCapabilitiesPath: '' }),
126+
},
127+
})
128+
);
129+
typeRegistry.registerType(
130+
createType({
131+
name: 'typeB',
132+
management: {
133+
defaultSearchField: 'description',
134+
getInAppUrl: (obj) => ({ path: `/type-b/${obj.id}`, uiCapabilitiesPath: 'foo' }),
135+
},
136+
})
137+
);
138+
typeRegistry.registerType(
139+
createType({
140+
name: 'typeC',
141+
management: {
142+
defaultSearchField: 'excerpt',
143+
getInAppUrl: (obj) => ({ path: `/type-c/${obj.id}`, uiCapabilitiesPath: 'bar' }),
144+
},
145+
})
146+
);
147+
148+
const results = [
149+
createObject(
150+
{
151+
id: 'resultA',
152+
type: 'typeA',
153+
score: 100,
154+
},
155+
{
156+
title: 'titleA',
157+
field: 'noise',
158+
}
159+
),
160+
createObject(
161+
{
162+
id: 'resultC',
163+
type: 'typeC',
164+
score: 42,
165+
},
166+
{
167+
excerpt: 'titleC',
168+
title: 'foo',
169+
}
170+
),
171+
createObject(
172+
{
173+
id: 'resultB',
174+
type: 'typeB',
175+
score: 69,
176+
},
177+
{
178+
description: 'titleB',
179+
bar: 'baz',
180+
}
181+
),
182+
];
183+
184+
expect(mapToResults(results, typeRegistry)).toEqual([
185+
{
186+
id: 'resultA',
187+
title: 'titleA',
188+
type: 'typeA',
189+
url: '/type-a/resultA',
190+
score: 100,
191+
},
192+
{
193+
id: 'resultC',
194+
title: 'titleC',
195+
type: 'typeC',
196+
url: '/type-c/resultC',
197+
score: 42,
198+
},
199+
{
200+
id: 'resultB',
201+
title: 'titleB',
202+
type: 'typeB',
203+
url: '/type-b/resultB',
204+
score: 69,
205+
},
206+
]);
207+
});
208+
});

0 commit comments

Comments
 (0)