Skip to content

Commit

Permalink
Include subLinks in GS application results
Browse files Browse the repository at this point in the history
  • Loading branch information
joshdover committed Nov 24, 2020
1 parent e9f6469 commit c7fcaf1
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const createApp = (props: Partial<PublicAppInfo> = {}): PublicAppInfo => ({
status: AppStatus.accessible,
navLinkStatus: AppNavLinkStatus.visible,
chromeless: false,
subLinks: [],
...props,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
PublicAppInfo,
DEFAULT_APP_CATEGORIES,
} from 'src/core/public';
import { appToResult, getAppResults, scoreApp } from './get_app_results';
import { AppLink, appToResult, getAppResults, scoreApp } from './get_app_results';

const createApp = (props: Partial<PublicAppInfo> = {}): PublicAppInfo => ({
id: 'app1',
Expand All @@ -19,9 +19,17 @@ const createApp = (props: Partial<PublicAppInfo> = {}): PublicAppInfo => ({
status: AppStatus.accessible,
navLinkStatus: AppNavLinkStatus.visible,
chromeless: false,
subLinks: [],
...props,
});

const createAppLink = (props: Partial<PublicAppInfo> = {}): AppLink => ({
id: props.id ?? 'app1',
path: props.appRoute ?? '/app/app1',
subLinkTitles: [],
app: createApp(props),
});

describe('getAppResults', () => {
it('retrieves the matching results', () => {
const apps = [
Expand All @@ -34,43 +42,80 @@ describe('getAppResults', () => {
expect(results.length).toBe(1);
expect(results[0]).toEqual(expect.objectContaining({ id: 'dashboard', score: 100 }));
});

it('creates multiple links for apps with sublinks', () => {
const apps = [
createApp({
subLinks: [
{ id: 'sub1', title: 'Sub1', path: '/sub1', subLinks: [] },
{
id: 'sub2',
title: 'Sub2',
path: '/sub2',
subLinks: [{ id: 'sub2sub1', title: 'Sub2Sub1', path: '/sub2/sub1', subLinks: [] }],
},
],
}),
];

const results = getAppResults('App 1', apps);

expect(results.length).toBe(4);
expect(results.map(({ title }) => title)).toEqual([
'App 1',
'App 1 / Sub1',
'App 1 / Sub2',
'App 1 / Sub2 / Sub2Sub1',
]);
});

it('only includes sublinks when search term is non-empty', () => {
const apps = [
createApp({
subLinks: [{ id: 'sub1', title: 'Sub1', path: '/sub1', subLinks: [] }],
}),
];

expect(getAppResults('', apps).length).toBe(1);
expect(getAppResults('App 1', apps).length).toBe(2);
});
});

describe('scoreApp', () => {
describe('when the term is included in the title', () => {
it('returns 100 if the app title is an exact match', () => {
expect(scoreApp('dashboard', createApp({ title: 'dashboard' }))).toBe(100);
expect(scoreApp('dashboard', createApp({ title: 'DASHBOARD' }))).toBe(100);
expect(scoreApp('DASHBOARD', createApp({ title: 'DASHBOARD' }))).toBe(100);
expect(scoreApp('dashBOARD', createApp({ title: 'DASHboard' }))).toBe(100);
expect(scoreApp('dashboard', createAppLink({ title: 'dashboard' }))).toBe(100);
expect(scoreApp('dashboard', createAppLink({ title: 'DASHBOARD' }))).toBe(100);
expect(scoreApp('DASHBOARD', createAppLink({ title: 'DASHBOARD' }))).toBe(100);
expect(scoreApp('dashBOARD', createAppLink({ title: 'DASHboard' }))).toBe(100);
});

it('returns 90 if the app title starts with the term', () => {
expect(scoreApp('dash', createApp({ title: 'dashboard' }))).toBe(90);
expect(scoreApp('DASH', createApp({ title: 'dashboard' }))).toBe(90);
expect(scoreApp('dash', createAppLink({ title: 'dashboard' }))).toBe(90);
expect(scoreApp('DASH', createAppLink({ title: 'dashboard' }))).toBe(90);
});

it('returns 75 if the term in included in the app title', () => {
expect(scoreApp('board', createApp({ title: 'dashboard' }))).toBe(75);
expect(scoreApp('shboa', createApp({ title: 'dashboard' }))).toBe(75);
expect(scoreApp('board', createAppLink({ title: 'dashboard' }))).toBe(75);
expect(scoreApp('shboa', createAppLink({ title: 'dashboard' }))).toBe(75);
});
});

describe('when the term is not included in the title', () => {
it('returns the levenshtein ratio if superior or equal to 60', () => {
expect(scoreApp('0123456789', createApp({ title: '012345' }))).toBe(60);
expect(scoreApp('--1234567-', createApp({ title: '123456789' }))).toBe(60);
expect(scoreApp('0123456789', createAppLink({ title: '012345' }))).toBe(60);
expect(scoreApp('--1234567-', createAppLink({ title: '123456789' }))).toBe(60);
});
it('returns 0 if the levenshtein ratio is inferior to 60', () => {
expect(scoreApp('0123456789', createApp({ title: '12345' }))).toBe(0);
expect(scoreApp('1-2-3-4-5', createApp({ title: '123456789' }))).toBe(0);
expect(scoreApp('0123456789', createAppLink({ title: '12345' }))).toBe(0);
expect(scoreApp('1-2-3-4-5', createAppLink({ title: '123456789' }))).toBe(0);
});
});
});

describe('appToResult', () => {
it('converts an app to a result', () => {
const app = createApp({
const app = createAppLink({
id: 'foo',
title: 'Foo',
euiIconType: 'fooIcon',
Expand All @@ -92,7 +137,7 @@ describe('appToResult', () => {
});

it('converts an app without category to a result', () => {
const app = createApp({
const app = createAppLink({
id: 'foo',
title: 'Foo',
euiIconType: 'fooIcon',
Expand All @@ -111,4 +156,28 @@ describe('appToResult', () => {
score: 42,
});
});

it('includes the app name in sub links', () => {
const app = createApp();
const appLink: AppLink = {
id: 'app1-sub',
app,
path: '/sub1',
subLinkTitles: ['Sub1'],
};

expect(appToResult(appLink, 42).title).toEqual('App 1 / Sub1');
});

it('does not include the app name in sub links for Stack Management', () => {
const app = createApp({ id: 'management' });
const appLink: AppLink = {
id: 'management-sub',
app,
path: '/sub1',
subLinkTitles: ['Sub1'],
};

expect(appToResult(appLink, 42).title).toEqual('Sub1');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,39 @@
*/

import levenshtein from 'js-levenshtein';
import { PublicAppInfo } from 'src/core/public';
import { PublicAppInfo, PublicAppSubLinkInfo } from 'src/core/public';
import { GlobalSearchProviderResult } from '../../../global_search/public';

/** Type used internally to represent an application unrolled into its separate sublinks */
export interface AppLink {
id: string;
app: PublicAppInfo;
subLinkTitles: string[];
path: string;
}

export const getAppResults = (
term: string,
apps: PublicAppInfo[]
): GlobalSearchProviderResult[] => {
return apps
.map((app) => ({ app, score: scoreApp(term, app) }))
.filter(({ score }) => score > 0)
.map(({ app, score }) => appToResult(app, score));
return (
apps
// Unroll all sublinks
.flatMap((app) => flattenSubLinks(app))
// Only include sublinks if there is a search term
.filter((appLink) => term.length > 0 || appLink.subLinkTitles.length === 0)
.map((appLink) => ({
appLink,
score: scoreApp(term, appLink),
}))
.filter(({ score }) => score > 0)
.map(({ appLink, score }) => appToResult(appLink, score))
);
};

export const scoreApp = (term: string, { title }: PublicAppInfo): number => {
export const scoreApp = (term: string, appLink: AppLink): number => {
term = term.toLowerCase();
title = title.toLowerCase();
const title = [appLink.app.title, ...appLink.subLinkTitles].join(' ').toLowerCase();

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

export const appToResult = (app: PublicAppInfo, score: number): GlobalSearchProviderResult => {
export const appToResult = (appLink: AppLink, score: number): GlobalSearchProviderResult => {
const titleParts =
// Stack Management app should not include the app title in the concatenated link label
appLink.app.id === 'management' && appLink.subLinkTitles.length > 0
? appLink.subLinkTitles
: [appLink.app.title, ...appLink.subLinkTitles];

return {
id: app.id,
title: app.title,
id: appLink.id,
// Concatenate title using slashes
title: titleParts.join(' / '),
type: 'application',
icon: app.euiIconType,
url: app.appRoute,
icon: appLink.app.euiIconType,
url: appLink.path,
meta: {
categoryId: app.category?.id ?? null,
categoryLabel: app.category?.label ?? null,
categoryId: appLink.app.category?.id ?? null,
categoryLabel: appLink.app.category?.label ?? null,
},
score,
};
};

const flattenSubLinks = (app: PublicAppInfo, subLink?: PublicAppSubLinkInfo): AppLink[] => {
if (!subLink) {
return [
{
id: app.id,
app,
path: app.appRoute,
subLinkTitles: [],
},
...app.subLinks.flatMap((appSubLink) => flattenSubLinks(app, appSubLink)),
];
}

const appLink: AppLink = {
id: `${app.id}-${subLink.id}`,
app,
subLinkTitles: [subLink.title],
path: `${app.appRoute}${subLink.path}`,
};

return [
...(subLink.path ? [appLink] : []),
...subLink.subLinks
.flatMap((subSubLink) => flattenSubLinks(app, subSubLink))
.map((subAppLink) => ({
...subAppLink,
// shift current sublink title into array of sub-sublink titles
subLinkTitles: [subLink.title, ...subAppLink.subLinkTitles],
})),
];
};
Loading

0 comments on commit c7fcaf1

Please sign in to comment.