Skip to content

Commit

Permalink
[Fleet] Show beats replacements in integration browser (#113291)
Browse files Browse the repository at this point in the history
Display both beats and epr-packages in the integration browser. When there is overlap, the EPR-package equivalent is displayed. When the EPR-package is not yet ga, the beat-equivalent is displayed.
  • Loading branch information
thomasneirynck authored Oct 1, 2021
1 parent 56effce commit a565eaa
Show file tree
Hide file tree
Showing 29 changed files with 542 additions and 73 deletions.
14 changes: 8 additions & 6 deletions src/plugins/custom_integrations/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
export const PLUGIN_ID = 'customIntegrations';
export const PLUGIN_NAME = 'customIntegrations';

export interface CategoryCount {
export interface IntegrationCategoryCount {
count: number;
id: Category;
id: IntegrationCategory;
}

export const CATEGORY_DISPLAY = {
export const INTEGRATION_CATEGORY_DISPLAY = {
aws: 'AWS',
azure: 'Azure',
cloud: 'Cloud',
Expand Down Expand Up @@ -44,7 +44,7 @@ export const CATEGORY_DISPLAY = {
updates_available: 'Updates available',
};

export type Category = keyof typeof CATEGORY_DISPLAY;
export type IntegrationCategory = keyof typeof INTEGRATION_CATEGORY_DISPLAY;

export interface CustomIntegrationIcon {
src: string;
Expand All @@ -59,8 +59,10 @@ export interface CustomIntegration {
uiInternalPath: string;
isBeta: boolean;
icons: CustomIntegrationIcon[];
categories: Category[];
categories: IntegrationCategory[];
shipper: string;
eprOverlap?: string; // name of the equivalent Elastic Agent integration in EPR. e.g. a beat module can correspond to an EPR-package, or an APM-tutorial. When completed, Integrations-UX can preferentially show the EPR-package, rather than the custom-integration
}

export const ROUTES_ADDABLECUSTOMINTEGRATIONS = `/api/${PLUGIN_ID}/appendCustomIntegrations`;
export const ROUTES_APPEND_CUSTOM_INTEGRATIONS = `/internal/${PLUGIN_ID}/appendCustomIntegrations`;
export const ROUTES_REPLACEMENT_CUSTOM_INTEGRATIONS = `/internal/${PLUGIN_ID}/replacementCustomIntegrations`;
4 changes: 2 additions & 2 deletions src/plugins/custom_integrations/public/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
import { CustomIntegrationsSetup } from './types';

function createCustomIntegrationsSetup(): jest.Mocked<CustomIntegrationsSetup> {
const mock = {
const mock: jest.Mocked<CustomIntegrationsSetup> = {
getAppendCustomIntegrations: jest.fn(),
getReplacementCustomIntegrations: jest.fn(),
};

return mock;
}

Expand Down
14 changes: 11 additions & 3 deletions src/plugins/custom_integrations/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,26 @@

import { CoreSetup, CoreStart, Plugin } from 'src/core/public';
import { CustomIntegrationsSetup, CustomIntegrationsStart } from './types';
import { CustomIntegration, ROUTES_ADDABLECUSTOMINTEGRATIONS } from '../common';
import {
CustomIntegration,
ROUTES_APPEND_CUSTOM_INTEGRATIONS,
ROUTES_REPLACEMENT_CUSTOM_INTEGRATIONS,
} from '../common';

export class CustomIntegrationsPlugin
implements Plugin<CustomIntegrationsSetup, CustomIntegrationsStart>
{
public setup(core: CoreSetup): CustomIntegrationsSetup {
// Return methods that should be available to other plugins
return {
async getReplacementCustomIntegrations(): Promise<CustomIntegration[]> {
return core.http.get(ROUTES_REPLACEMENT_CUSTOM_INTEGRATIONS);
},

async getAppendCustomIntegrations(): Promise<CustomIntegration[]> {
return core.http.get(ROUTES_ADDABLECUSTOMINTEGRATIONS);
return core.http.get(ROUTES_APPEND_CUSTOM_INTEGRATIONS);
},
} as CustomIntegrationsSetup;
};
}

public start(core: CoreStart): CustomIntegrationsStart {
Expand Down
1 change: 1 addition & 0 deletions src/plugins/custom_integrations/public/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { CustomIntegration } from '../common';

export interface CustomIntegrationsSetup {
getAppendCustomIntegrations: () => Promise<CustomIntegration[]>;
getReplacementCustomIntegrations: () => Promise<CustomIntegration[]>;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface CustomIntegrationsStart {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import { CustomIntegrationRegistry } from './custom_integration_registry';
import { loggerMock, MockedLogger } from '@kbn/logging/mocks';
import { CustomIntegration } from '../common';
import { IntegrationCategory, CustomIntegration } from '../common';

describe('CustomIntegrationsRegistry', () => {
let mockLogger: MockedLogger;
Expand Down Expand Up @@ -44,6 +44,27 @@ describe('CustomIntegrationsRegistry', () => {
expect(mockLogger.debug.mock.calls.length).toBe(1);
});
});

test('should strip unsupported categories', () => {
const registry = new CustomIntegrationRegistry(mockLogger, true);
registry.registerCustomIntegration({
...integration,
categories: ['upload_file', 'foobar'] as IntegrationCategory[],
});
expect(registry.getAppendCustomIntegrations()).toEqual([
{
categories: ['upload_file'],
description: 'test integration',
icons: [],
id: 'foo',
isBeta: false,
shipper: 'tests',
title: 'Foo',
type: 'ui_link',
uiInternalPath: '/path/to/foo',
},
]);
});
});

describe('getAppendCustomCategories', () => {
Expand Down Expand Up @@ -76,7 +97,7 @@ describe('CustomIntegrationsRegistry', () => {
},
]);
});
test('should ignore duplicate ids', () => {
test('should filter duplicate ids', () => {
const registry = new CustomIntegrationRegistry(mockLogger, true);
registry.registerCustomIntegration(integration);
registry.registerCustomIntegration(integration);
Expand All @@ -94,7 +115,7 @@ describe('CustomIntegrationsRegistry', () => {
},
]);
});
test('should ignore integrations without category', () => {
test('should filter integrations without category', () => {
const registry = new CustomIntegrationRegistry(mockLogger, true);
registry.registerCustomIntegration(integration);
registry.registerCustomIntegration({ ...integration, id: 'bar', categories: [] });
Expand All @@ -113,5 +134,44 @@ describe('CustomIntegrationsRegistry', () => {
},
]);
});

test('should filter integrations that need to replace EPR packages', () => {
const registry = new CustomIntegrationRegistry(mockLogger, true);
registry.registerCustomIntegration({ ...integration, id: 'bar', eprOverlap: 'aws' });
expect(registry.getAppendCustomIntegrations()).toEqual([]);
});
});

describe('getReplacementCustomIntegrations', () => {
test('should only return integrations with corresponding epr package ', () => {
const registry = new CustomIntegrationRegistry(mockLogger, true);
registry.registerCustomIntegration(integration);
registry.registerCustomIntegration({ ...integration, id: 'bar', eprOverlap: 'aws' });
expect(registry.getReplacementCustomIntegrations()).toEqual([
{
categories: ['upload_file'],
description: 'test integration',
icons: [],
id: 'bar',
isBeta: false,
shipper: 'tests',
title: 'Foo',
type: 'ui_link',
uiInternalPath: '/path/to/foo',
eprOverlap: 'aws',
},
]);
});

test('should filter registrations without valid categories', () => {
const registry = new CustomIntegrationRegistry(mockLogger, true);
registry.registerCustomIntegration({
...integration,
id: 'bar',
eprOverlap: 'aws',
categories: ['foobar'] as unknown as IntegrationCategory[],
});
expect(registry.getReplacementCustomIntegrations()).toEqual([]);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@
*/

import { Logger } from 'kibana/server';
import { CustomIntegration } from '../common';
import { IntegrationCategory, INTEGRATION_CATEGORY_DISPLAY, CustomIntegration } from '../common';

function isAddable(integration: CustomIntegration) {
return integration.categories.length;
function isAddable(integration: CustomIntegration): boolean {
return !!integration.categories.length && !integration.eprOverlap;
}

function isReplacement(integration: CustomIntegration): boolean {
return !!integration.categories.length && !!integration.eprOverlap;
}

export class CustomIntegrationRegistry {
Expand Down Expand Up @@ -39,10 +43,20 @@ export class CustomIntegrationRegistry {
return;
}

this._integrations.push(customIntegration);
const allowedCategories: IntegrationCategory[] = (customIntegration.categories ?? []).filter(
(category) => {
return INTEGRATION_CATEGORY_DISPLAY.hasOwnProperty(category);
}
) as IntegrationCategory[];

this._integrations.push({ ...customIntegration, categories: allowedCategories });
}

getAppendCustomIntegrations(): CustomIntegration[] {
return this._integrations.filter(isAddable);
}

getReplacementCustomIntegrations(): CustomIntegration[] {
return this._integrations.filter(isReplacement);
}
}
2 changes: 1 addition & 1 deletion src/plugins/custom_integrations/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function plugin(initializerContext: PluginInitializerContext) {

export { CustomIntegrationsPluginSetup, CustomIntegrationsPluginStart } from './types';

export type { Category, CategoryCount, CustomIntegration } from '../common';
export type { IntegrationCategory, IntegrationCategoryCount, CustomIntegration } from '../common';

export const config = {
schema: schema.object({}),
Expand Down
1 change: 0 additions & 1 deletion src/plugins/custom_integrations/server/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ describe('CustomIntegrationsPlugin', () => {
test('wires up tutorials provider service and returns registerTutorial and addScopedTutorialContextFactory', () => {
const setup = new CustomIntegrationsPlugin(initContext).setup(mockCoreSetup);
expect(setup).toHaveProperty('registerCustomIntegration');
expect(setup).toHaveProperty('getAppendCustomIntegrations');
});
});
});
3 changes: 0 additions & 3 deletions src/plugins/custom_integrations/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,6 @@ export class CustomIntegrationsPlugin
...integration,
});
},
getAppendCustomIntegrations: (): CustomIntegration[] => {
return this.customIngegrationRegistry.getAppendCustomIntegrations();
},
} as CustomIntegrationsPluginSetup;
}

Expand Down
20 changes: 18 additions & 2 deletions src/plugins/custom_integrations/server/routes/define_routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,18 @@

import { IRouter } from 'src/core/server';
import { CustomIntegrationRegistry } from '../custom_integration_registry';
import { ROUTES_ADDABLECUSTOMINTEGRATIONS } from '../../common';
import {
ROUTES_APPEND_CUSTOM_INTEGRATIONS,
ROUTES_REPLACEMENT_CUSTOM_INTEGRATIONS,
} from '../../common';

export function defineRoutes(
router: IRouter,
customIntegrationsRegistry: CustomIntegrationRegistry
) {
router.get(
{
path: ROUTES_ADDABLECUSTOMINTEGRATIONS,
path: ROUTES_APPEND_CUSTOM_INTEGRATIONS,
validate: false,
},
async (context, request, response) => {
Expand All @@ -26,4 +29,17 @@ export function defineRoutes(
});
}
);

router.get(
{
path: ROUTES_REPLACEMENT_CUSTOM_INTEGRATIONS,
validate: false,
},
async (context, request, response) => {
const integrations = customIntegrationsRegistry.getReplacementCustomIntegrations();
return response.ok({
body: integrations,
});
}
);
}
2 changes: 1 addition & 1 deletion src/plugins/home/server/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ describe('HomeServerPlugin', () => {
test('is defined', () => {
const plugin = new HomeServerPlugin(initContext);
plugin.setup(mockCoreSetup, homeServerPluginSetupDependenciesMock); // setup() must always be called before start()
const start = plugin.start();
const start = plugin.start(coreMock.createStart());
expect(start).toBeDefined();
expect(start).toHaveProperty('tutorials');
expect(start).toHaveProperty('sampleData');
Expand Down
9 changes: 6 additions & 3 deletions src/plugins/home/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Side Public License, v 1.
*/

import { CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server';
import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'kibana/server';
import {
TutorialsRegistry,
TutorialsRegistrySetup,
Expand All @@ -30,8 +30,11 @@ export class HomeServerPlugin implements Plugin<HomeServerPluginSetup, HomeServe
constructor(private readonly initContext: PluginInitializerContext) {}
private readonly tutorialsRegistry = new TutorialsRegistry();
private readonly sampleDataRegistry = new SampleDataRegistry(this.initContext);
private customIntegrations?: CustomIntegrationsPluginSetup;

public setup(core: CoreSetup, plugins: HomeServerPluginSetupDependencies): HomeServerPluginSetup {
this.customIntegrations = plugins.customIntegrations;

core.capabilities.registerProvider(capabilitiesProvider);
core.savedObjects.registerType(sampleDataTelemetry);

Expand All @@ -46,9 +49,9 @@ export class HomeServerPlugin implements Plugin<HomeServerPluginSetup, HomeServe
};
}

public start(): HomeServerPluginStart {
public start(core: CoreStart): HomeServerPluginStart {
return {
tutorials: { ...this.tutorialsRegistry.start() },
tutorials: { ...this.tutorialsRegistry.start(core, this.customIntegrations) },
sampleData: { ...this.sampleDataRegistry.start() },
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,11 @@ export const tutorialSchema = schema.object({
savedObjectsInstallMsg: schema.maybe(schema.string()),
customStatusCheckName: schema.maybe(schema.string()),

// Category assignment for the integration browser
integrationBrowserCategories: schema.maybe(schema.arrayOf(schema.string())),

// Name of an equivalent package in EPR. e.g. this needs to be explicitly defined if it cannot be derived from a heuristic.
eprPackageOverlap: schema.maybe(schema.string()),
});

export type TutorialSchema = TypeOf<typeof tutorialSchema>;
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,10 @@ describe('TutorialsRegistry', () => {

describe('start', () => {
test('exposes proper contract', () => {
const start = new TutorialsRegistry().start();
const start = new TutorialsRegistry().start(
coreMock.createStart(),
mockCustomIntegrationsPluginSetup
);
expect(start).toBeDefined();
});
});
Expand Down
Loading

0 comments on commit a565eaa

Please sign in to comment.