Skip to content

Commit c7fcaf1

Browse files
committed
Include subLinks in GS application results
1 parent e9f6469 commit c7fcaf1

File tree

4 files changed

+216
-74
lines changed

4 files changed

+216
-74
lines changed

x-pack/plugins/global_search_providers/public/providers/application.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const createApp = (props: Partial<PublicAppInfo> = {}): PublicAppInfo => ({
2828
status: AppStatus.accessible,
2929
navLinkStatus: AppNavLinkStatus.visible,
3030
chromeless: false,
31+
subLinks: [],
3132
...props,
3233
});
3334

x-pack/plugins/global_search_providers/public/providers/get_app_results.test.ts

Lines changed: 84 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
PublicAppInfo,
1111
DEFAULT_APP_CATEGORIES,
1212
} from 'src/core/public';
13-
import { appToResult, getAppResults, scoreApp } from './get_app_results';
13+
import { AppLink, appToResult, getAppResults, scoreApp } from './get_app_results';
1414

1515
const createApp = (props: Partial<PublicAppInfo> = {}): PublicAppInfo => ({
1616
id: 'app1',
@@ -19,9 +19,17 @@ const createApp = (props: Partial<PublicAppInfo> = {}): PublicAppInfo => ({
1919
status: AppStatus.accessible,
2020
navLinkStatus: AppNavLinkStatus.visible,
2121
chromeless: false,
22+
subLinks: [],
2223
...props,
2324
});
2425

26+
const createAppLink = (props: Partial<PublicAppInfo> = {}): AppLink => ({
27+
id: props.id ?? 'app1',
28+
path: props.appRoute ?? '/app/app1',
29+
subLinkTitles: [],
30+
app: createApp(props),
31+
});
32+
2533
describe('getAppResults', () => {
2634
it('retrieves the matching results', () => {
2735
const apps = [
@@ -34,43 +42,80 @@ describe('getAppResults', () => {
3442
expect(results.length).toBe(1);
3543
expect(results[0]).toEqual(expect.objectContaining({ id: 'dashboard', score: 100 }));
3644
});
45+
46+
it('creates multiple links for apps with sublinks', () => {
47+
const apps = [
48+
createApp({
49+
subLinks: [
50+
{ id: 'sub1', title: 'Sub1', path: '/sub1', subLinks: [] },
51+
{
52+
id: 'sub2',
53+
title: 'Sub2',
54+
path: '/sub2',
55+
subLinks: [{ id: 'sub2sub1', title: 'Sub2Sub1', path: '/sub2/sub1', subLinks: [] }],
56+
},
57+
],
58+
}),
59+
];
60+
61+
const results = getAppResults('App 1', apps);
62+
63+
expect(results.length).toBe(4);
64+
expect(results.map(({ title }) => title)).toEqual([
65+
'App 1',
66+
'App 1 / Sub1',
67+
'App 1 / Sub2',
68+
'App 1 / Sub2 / Sub2Sub1',
69+
]);
70+
});
71+
72+
it('only includes sublinks when search term is non-empty', () => {
73+
const apps = [
74+
createApp({
75+
subLinks: [{ id: 'sub1', title: 'Sub1', path: '/sub1', subLinks: [] }],
76+
}),
77+
];
78+
79+
expect(getAppResults('', apps).length).toBe(1);
80+
expect(getAppResults('App 1', apps).length).toBe(2);
81+
});
3782
});
3883

3984
describe('scoreApp', () => {
4085
describe('when the term is included in the title', () => {
4186
it('returns 100 if the app title is an exact match', () => {
42-
expect(scoreApp('dashboard', createApp({ title: 'dashboard' }))).toBe(100);
43-
expect(scoreApp('dashboard', createApp({ title: 'DASHBOARD' }))).toBe(100);
44-
expect(scoreApp('DASHBOARD', createApp({ title: 'DASHBOARD' }))).toBe(100);
45-
expect(scoreApp('dashBOARD', createApp({ title: 'DASHboard' }))).toBe(100);
87+
expect(scoreApp('dashboard', createAppLink({ title: 'dashboard' }))).toBe(100);
88+
expect(scoreApp('dashboard', createAppLink({ title: 'DASHBOARD' }))).toBe(100);
89+
expect(scoreApp('DASHBOARD', createAppLink({ title: 'DASHBOARD' }))).toBe(100);
90+
expect(scoreApp('dashBOARD', createAppLink({ title: 'DASHboard' }))).toBe(100);
4691
});
4792

4893
it('returns 90 if the app title starts with the term', () => {
49-
expect(scoreApp('dash', createApp({ title: 'dashboard' }))).toBe(90);
50-
expect(scoreApp('DASH', createApp({ title: 'dashboard' }))).toBe(90);
94+
expect(scoreApp('dash', createAppLink({ title: 'dashboard' }))).toBe(90);
95+
expect(scoreApp('DASH', createAppLink({ title: 'dashboard' }))).toBe(90);
5196
});
5297

5398
it('returns 75 if the term in included in the app title', () => {
54-
expect(scoreApp('board', createApp({ title: 'dashboard' }))).toBe(75);
55-
expect(scoreApp('shboa', createApp({ title: 'dashboard' }))).toBe(75);
99+
expect(scoreApp('board', createAppLink({ title: 'dashboard' }))).toBe(75);
100+
expect(scoreApp('shboa', createAppLink({ title: 'dashboard' }))).toBe(75);
56101
});
57102
});
58103

59104
describe('when the term is not included in the title', () => {
60105
it('returns the levenshtein ratio if superior or equal to 60', () => {
61-
expect(scoreApp('0123456789', createApp({ title: '012345' }))).toBe(60);
62-
expect(scoreApp('--1234567-', createApp({ title: '123456789' }))).toBe(60);
106+
expect(scoreApp('0123456789', createAppLink({ title: '012345' }))).toBe(60);
107+
expect(scoreApp('--1234567-', createAppLink({ title: '123456789' }))).toBe(60);
63108
});
64109
it('returns 0 if the levenshtein ratio is inferior to 60', () => {
65-
expect(scoreApp('0123456789', createApp({ title: '12345' }))).toBe(0);
66-
expect(scoreApp('1-2-3-4-5', createApp({ title: '123456789' }))).toBe(0);
110+
expect(scoreApp('0123456789', createAppLink({ title: '12345' }))).toBe(0);
111+
expect(scoreApp('1-2-3-4-5', createAppLink({ title: '123456789' }))).toBe(0);
67112
});
68113
});
69114
});
70115

71116
describe('appToResult', () => {
72117
it('converts an app to a result', () => {
73-
const app = createApp({
118+
const app = createAppLink({
74119
id: 'foo',
75120
title: 'Foo',
76121
euiIconType: 'fooIcon',
@@ -92,7 +137,7 @@ describe('appToResult', () => {
92137
});
93138

94139
it('converts an app without category to a result', () => {
95-
const app = createApp({
140+
const app = createAppLink({
96141
id: 'foo',
97142
title: 'Foo',
98143
euiIconType: 'fooIcon',
@@ -111,4 +156,28 @@ describe('appToResult', () => {
111156
score: 42,
112157
});
113158
});
159+
160+
it('includes the app name in sub links', () => {
161+
const app = createApp();
162+
const appLink: AppLink = {
163+
id: 'app1-sub',
164+
app,
165+
path: '/sub1',
166+
subLinkTitles: ['Sub1'],
167+
};
168+
169+
expect(appToResult(appLink, 42).title).toEqual('App 1 / Sub1');
170+
});
171+
172+
it('does not include the app name in sub links for Stack Management', () => {
173+
const app = createApp({ id: 'management' });
174+
const appLink: AppLink = {
175+
id: 'management-sub',
176+
app,
177+
path: '/sub1',
178+
subLinkTitles: ['Sub1'],
179+
};
180+
181+
expect(appToResult(appLink, 42).title).toEqual('Sub1');
182+
});
114183
});

x-pack/plugins/global_search_providers/public/providers/get_app_results.ts

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,39 @@
55
*/
66

77
import levenshtein from 'js-levenshtein';
8-
import { PublicAppInfo } from 'src/core/public';
8+
import { PublicAppInfo, PublicAppSubLinkInfo } from 'src/core/public';
99
import { GlobalSearchProviderResult } from '../../../global_search/public';
1010

11+
/** Type used internally to represent an application unrolled into its separate sublinks */
12+
export interface AppLink {
13+
id: string;
14+
app: PublicAppInfo;
15+
subLinkTitles: string[];
16+
path: string;
17+
}
18+
1119
export const getAppResults = (
1220
term: string,
1321
apps: PublicAppInfo[]
1422
): GlobalSearchProviderResult[] => {
15-
return apps
16-
.map((app) => ({ app, score: scoreApp(term, app) }))
17-
.filter(({ score }) => score > 0)
18-
.map(({ app, score }) => appToResult(app, score));
23+
return (
24+
apps
25+
// Unroll all sublinks
26+
.flatMap((app) => flattenSubLinks(app))
27+
// Only include sublinks if there is a search term
28+
.filter((appLink) => term.length > 0 || appLink.subLinkTitles.length === 0)
29+
.map((appLink) => ({
30+
appLink,
31+
score: scoreApp(term, appLink),
32+
}))
33+
.filter(({ score }) => score > 0)
34+
.map(({ appLink, score }) => appToResult(appLink, score))
35+
);
1936
};
2037

21-
export const scoreApp = (term: string, { title }: PublicAppInfo): number => {
38+
export const scoreApp = (term: string, appLink: AppLink): number => {
2239
term = term.toLowerCase();
23-
title = title.toLowerCase();
40+
const title = [appLink.app.title, ...appLink.subLinkTitles].join(' ').toLowerCase();
2441

2542
// shortcuts to avoid calculating the distance when there is an exact match somewhere.
2643
if (title === term) {
@@ -43,17 +60,56 @@ export const scoreApp = (term: string, { title }: PublicAppInfo): number => {
4360
return 0;
4461
};
4562

46-
export const appToResult = (app: PublicAppInfo, score: number): GlobalSearchProviderResult => {
63+
export const appToResult = (appLink: AppLink, score: number): GlobalSearchProviderResult => {
64+
const titleParts =
65+
// Stack Management app should not include the app title in the concatenated link label
66+
appLink.app.id === 'management' && appLink.subLinkTitles.length > 0
67+
? appLink.subLinkTitles
68+
: [appLink.app.title, ...appLink.subLinkTitles];
69+
4770
return {
48-
id: app.id,
49-
title: app.title,
71+
id: appLink.id,
72+
// Concatenate title using slashes
73+
title: titleParts.join(' / '),
5074
type: 'application',
51-
icon: app.euiIconType,
52-
url: app.appRoute,
75+
icon: appLink.app.euiIconType,
76+
url: appLink.path,
5377
meta: {
54-
categoryId: app.category?.id ?? null,
55-
categoryLabel: app.category?.label ?? null,
78+
categoryId: appLink.app.category?.id ?? null,
79+
categoryLabel: appLink.app.category?.label ?? null,
5680
},
5781
score,
5882
};
5983
};
84+
85+
const flattenSubLinks = (app: PublicAppInfo, subLink?: PublicAppSubLinkInfo): AppLink[] => {
86+
if (!subLink) {
87+
return [
88+
{
89+
id: app.id,
90+
app,
91+
path: app.appRoute,
92+
subLinkTitles: [],
93+
},
94+
...app.subLinks.flatMap((appSubLink) => flattenSubLinks(app, appSubLink)),
95+
];
96+
}
97+
98+
const appLink: AppLink = {
99+
id: `${app.id}-${subLink.id}`,
100+
app,
101+
subLinkTitles: [subLink.title],
102+
path: `${app.appRoute}${subLink.path}`,
103+
};
104+
105+
return [
106+
...(subLink.path ? [appLink] : []),
107+
...subLink.subLinks
108+
.flatMap((subSubLink) => flattenSubLinks(app, subSubLink))
109+
.map((subAppLink) => ({
110+
...subAppLink,
111+
// shift current sublink title into array of sub-sublink titles
112+
subLinkTitles: [subLink.title, ...subAppLink.subLinkTitles],
113+
})),
114+
];
115+
};

0 commit comments

Comments
 (0)