[Global Search] Register custom integrations search provider (#213013)

## Summary

This PR creates search provider for custom integrations so they show up
in Global Search.
Closes: #115778
This commit is contained in:
Krzysztof Kowalczyk 2025-03-05 00:00:33 +01:00 committed by GitHub
parent 98a7259ee1
commit c3c8f7befb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 201 additions and 2 deletions

View file

@ -76,7 +76,10 @@ import { CUSTOM_LOGS_INTEGRATION_NAME, INTEGRATIONS_BASE_PATH } from './constant
import type { RequestError } from './hooks';
import { licenseService, sendGetBulkAssets } from './hooks';
import { setHttpClient } from './hooks/use_request';
import { createPackageSearchProvider } from './search_provider';
import {
createCustomIntegrationsSearchProvider,
createPackageSearchProvider,
} from './search_provider';
import { TutorialDirectoryHeaderLink, TutorialModuleNotice } from './components/home_integration';
import { createExtensionRegistrationCallback } from './services/ui_extensions';
import { ExperimentalFeaturesService } from './services/experimental_features';
@ -291,6 +294,9 @@ export class FleetPlugin implements Plugin<FleetSetup, FleetStart, FleetSetupDep
if (deps.globalSearch) {
deps.globalSearch.registerResultProvider(createPackageSearchProvider(core));
deps.globalSearch.registerResultProvider(
createCustomIntegrationsSearchProvider(deps.customIntegrations)
);
}
return {};

View file

@ -9,8 +9,13 @@ import { TestScheduler } from 'rxjs/testing';
import { NEVER } from 'rxjs';
import { coreMock } from '@kbn/core/public/mocks';
import type { CustomIntegrationsSetup } from '@kbn/custom-integrations-plugin/public';
import { createPackageSearchProvider, toSearchResult } from './search_provider';
import {
createCustomIntegrationsSearchProvider,
createPackageSearchProvider,
toSearchResult,
} from './search_provider';
import type { GetPackagesResponse } from './types';
jest.mock('./hooks/use_request/epm', () => {
@ -667,3 +672,145 @@ describe('Package search provider', () => {
});
});
});
describe('Custom Integrations search provider', () => {
let customIntegrationsMock: CustomIntegrationsSetup;
const customIntegrationsMockData = [
{
id: 'custom1',
title: 'Custom Integration 1',
uiInternalPath: '/app/custom1',
icons: [{ src: 'icon1.svg' }],
},
{
id: 'custom2',
title: 'Custom Integration 2',
uiInternalPath: '/app/custom2',
icons: [{ src: 'icon2.svg' }],
},
];
beforeEach(() => {
customIntegrationsMock = {
getReplacementCustomIntegrations: jest.fn(),
} as unknown as CustomIntegrationsSetup;
});
afterEach(() => {
jest.clearAllMocks();
});
describe('#find', () => {
test('returns formatted results', () => {
getTestScheduler().run(({ expectObservable, hot }) => {
(customIntegrationsMock.getReplacementCustomIntegrations as jest.Mock).mockReturnValue(
hot('--a|', { a: customIntegrationsMockData })
);
const customIntegrationsSearchProvider =
createCustomIntegrationsSearchProvider(customIntegrationsMock);
expectObservable(
customIntegrationsSearchProvider.find(
{ term: 'custom' },
{ aborted$: NEVER, maxResults: 100, preference: '' }
)
).toBe('--a|', {
a: [
{
id: 'custom1',
score: 80,
title: 'Custom Integration 1',
type: 'integration',
url: '/app/custom1',
icon: 'icon1.svg',
},
{
id: 'custom2',
score: 80,
title: 'Custom Integration 2',
type: 'integration',
url: '/app/custom2',
icon: 'icon2.svg',
},
],
});
});
expect(customIntegrationsMock.getReplacementCustomIntegrations).toHaveBeenCalledTimes(1);
});
test('returns empty array if no term is provided', () => {
getTestScheduler().run(({ expectObservable, hot }) => {
(customIntegrationsMock.getReplacementCustomIntegrations as jest.Mock).mockReturnValue(
hot('--a|', { a: [] })
);
const customIntegrationsSearchProvider =
createCustomIntegrationsSearchProvider(customIntegrationsMock);
expectObservable(
customIntegrationsSearchProvider.find(
{ term: '' },
{ aborted$: NEVER, maxResults: 100, preference: '' }
)
).toBe('(a|)', {
a: [],
});
});
expect(customIntegrationsMock.getReplacementCustomIntegrations).toHaveBeenCalledTimes(0);
});
test('completes without returning results if aborted', () => {
getTestScheduler().run(({ expectObservable, hot }) => {
(customIntegrationsMock.getReplacementCustomIntegrations as jest.Mock).mockReturnValue(
hot('--a|', { a: customIntegrationsMockData })
);
const aborted$ = hot('-a', { a: undefined });
const customIntegrationsSearchProvider =
createCustomIntegrationsSearchProvider(customIntegrationsMock);
expectObservable(
customIntegrationsSearchProvider.find(
{ term: 'custom' },
{ aborted$, maxResults: 100, preference: '' }
)
).toBe('-|');
});
expect(customIntegrationsMock.getReplacementCustomIntegrations).toHaveBeenCalledTimes(1);
});
test('respects maximum results', () => {
getTestScheduler().run(({ hot, expectObservable }) => {
(customIntegrationsMock.getReplacementCustomIntegrations as jest.Mock).mockReturnValue(
hot('--a|', { a: customIntegrationsMockData })
);
const customIntegrationsSearchProvider =
createCustomIntegrationsSearchProvider(customIntegrationsMock);
expectObservable(
customIntegrationsSearchProvider.find(
{ term: 'custom' },
{ aborted$: NEVER, maxResults: 1, preference: '' }
)
).toBe('--a|', {
a: [
{
id: 'custom1',
score: 80,
title: 'Custom Integration 1',
type: 'integration',
url: '/app/custom1',
icon: 'icon1.svg',
},
],
});
});
expect(customIntegrationsMock.getReplacementCustomIntegrations).toHaveBeenCalledTimes(1);
});
});
});

View file

@ -15,6 +15,10 @@ import type {
GlobalSearchProviderResult,
} from '@kbn/global-search-plugin/public';
import type { CustomIntegrationsSetup } from '@kbn/custom-integrations-plugin/public';
import type { CustomIntegration } from '@kbn/custom-integrations-plugin/common';
import { INTEGRATIONS_PLUGIN_ID } from '../common';
import { filterPolicyTemplatesTiles } from '../common/services';
@ -147,3 +151,45 @@ export const createPackageSearchProvider = (core: CoreSetup): GlobalSearchResult
},
};
};
const toCustomItegrationSearchResult = (customIntegration: CustomIntegration) => ({
id: customIntegration.id,
type: packageType,
title: customIntegration.title,
score: 80,
url: customIntegration.uiInternalPath,
icon: customIntegration.icons.find(({ src }) => Boolean(src))?.src,
});
export const createCustomIntegrationsSearchProvider = (
customIntegrations: CustomIntegrationsSetup
): GlobalSearchResultProvider => {
return {
id: 'customIntegrations',
getSearchableTypes: () => [packageType],
find: ({ term, types }, { maxResults, aborted$ }) => {
if (types?.includes(packageType) === false) {
return of([]);
}
if (!term) {
return of([]);
}
const customIntegrations$ = from(customIntegrations.getReplacementCustomIntegrations()).pipe(
map((integrations) => integrations),
shareReplay(1)
);
return customIntegrations$.pipe(
takeUntil(aborted$),
map((customIntegrationsData) =>
customIntegrationsData
.map(toCustomItegrationSearchResult)
.filter((res) => term && res.title.toLowerCase().includes(term.toLowerCase()))
.slice(0, maxResults)
)
);
},
};
};