Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions src/plugins/custom_integrations/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,8 @@
"description": "Add custom data integrations so they can be displayed in the Fleet integrations app",
"ui": true,
"server": true,
"extraPublicDirs": [
"common"
],
"requiredPlugins": [
"presentationUtil"
],
"extraPublicDirs": ["common"],
"requiredPlugins": [],
"requiredBundles": [],
"optionalPlugins": []
}
2 changes: 1 addition & 1 deletion src/plugins/custom_integrations/public/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import { pluginServices } from './services';
import { PluginServiceRegistry } from '../../presentation_util/public';
import { PluginServiceRegistry } from './services/create';
import { CustomIntegrationsSetup, CustomIntegrationsStart } from './types';
import { CustomIntegrationsServices } from './services';
import { providers } from './services/stub';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { DependencyManager } from './dependency_manager';

describe('DependencyManager', () => {
it('orderDependencies. Should sort topology by dependencies', () => {
const graph = {
N: [],
R: [],
A: ['B', 'C'],
B: ['D'],
C: ['F', 'B'],
F: ['E'],
E: ['D'],
D: ['L'],
};
const sortedTopology = ['N', 'R', 'L', 'D', 'B', 'E', 'F', 'C', 'A'];
expect(DependencyManager.orderDependencies(graph)).toEqual(sortedTopology);
});

it('should include final vertex if it has dependencies', () => {
const graph = {
A: [],
B: [],
C: ['A', 'B'],
};
const sortedTopology = ['A', 'B', 'C'];
expect(DependencyManager.orderDependencies(graph)).toEqual(sortedTopology);
});

it('orderDependencies. Should return base topology if no depended vertices', () => {
const graph = {
N: [],
R: [],
D: undefined,
};
const sortedTopology = ['N', 'R', 'D'];
expect(DependencyManager.orderDependencies(graph)).toEqual(sortedTopology);
});

describe('circular dependencies', () => {
it('should detect circular dependencies and throw error with path', () => {
const graph = {
N: ['R'],
R: ['A'],
A: ['B'],
B: ['C'],
C: ['D'],
D: ['E'],
E: ['F'],
F: ['L'],
L: ['G'],
G: ['N'],
};
const circularPath = ['G', 'L', 'F', 'E', 'D', 'C', 'B', 'A', 'R', 'N'].join(' -> ');
const errorMessage = `Circular dependency detected while setting up services: ${circularPath}`;

expect(() => DependencyManager.orderDependencies(graph)).toThrowError(errorMessage);
});

it('should detect circular dependency if circular reference is the first dependency for a vertex', () => {
const graph = {
A: ['B'],
B: ['A', 'C'],
C: [],
};

expect(() => DependencyManager.orderDependencies(graph)).toThrow();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

type GraphVertex = string | number | symbol;
type Graph<T extends GraphVertex = GraphVertex> = Record<T, T[] | null | undefined>;
type BreadCrumbs = Record<GraphVertex, boolean>;

interface CycleDetectionResult<T extends GraphVertex = GraphVertex> {
hasCycle: boolean;
path: T[];
}

export class DependencyManager {
static orderDependencies<T extends GraphVertex = GraphVertex>(graph: Graph<T>) {
const cycleInfo = DependencyManager.getSortedDependencies(graph);
if (cycleInfo.hasCycle) {
const error = DependencyManager.getCyclePathError(cycleInfo.path);
DependencyManager.throwCyclicPathError(error);
}

return cycleInfo.path;
}

/**
* DFS algorithm for checking if graph is a DAG (Directed Acyclic Graph)
* and sorting topogy (dependencies) if graph is DAG.
* @param {Graph} graph - graph of dependencies.
*/
private static getSortedDependencies<T extends GraphVertex = GraphVertex>(
graph: Graph<T> = {} as Graph<T>
): CycleDetectionResult<T> {
const sortedVertices: Set<T> = new Set();
const vertices = Object.keys(graph) as T[];
return vertices.reduce<CycleDetectionResult<T>>((cycleInfo, srcVertex) => {
if (cycleInfo.hasCycle) {
return cycleInfo;
}

return DependencyManager.sortVerticesFrom(
srcVertex,
graph,
sortedVertices,
{},
{},
cycleInfo
);
}, DependencyManager.createCycleInfo());
}

/**
* Modified DFS algorithm for topological sort.
* @param {T extends GraphVertex} srcVertex - a source vertex - the start point of dependencies ordering.
* @param {Graph<T extends GraphVertex>} graph - graph of dependencies, represented in the adjacency list form.
* @param {Set<GraphVertex>} sortedVertices - ordered dependencies path from the free to the dependent vertex.
* @param {BreadCrumbs} visited - record of visited vertices.
* @param {BreadCrumbs} inpath - record of vertices, which was met in the path. Is used for detecting cycles.
*/
private static sortVerticesFrom<T extends GraphVertex = GraphVertex>(
srcVertex: T,
graph: Graph<T>,
sortedVertices: Set<T>,
visited: BreadCrumbs = {},
inpath: BreadCrumbs = {},
cycle: CycleDetectionResult<T>
): CycleDetectionResult<T> {
visited[srcVertex] = true;
inpath[srcVertex] = true;

const vertexEdges =
graph[srcVertex] === undefined || graph[srcVertex] === null ? [] : graph[srcVertex];

cycle = vertexEdges!.reduce<CycleDetectionResult<T>>((info, vertex) => {
if (inpath[vertex]) {
return { ...info, hasCycle: true };
} else if (!visited[vertex]) {
return DependencyManager.sortVerticesFrom(
vertex,
graph,
sortedVertices,
visited,
inpath,
info
);
}
return info;
}, cycle);

inpath[srcVertex] = false;

if (!sortedVertices.has(srcVertex)) {
sortedVertices.add(srcVertex);
}

return {
...cycle,
path: [...sortedVertices],
};
}

private static createCycleInfo<T extends GraphVertex = GraphVertex>(
path: T[] = [],
hasCycle: boolean = false
): CycleDetectionResult<T> {
return { hasCycle, path };
}

private static getCyclePathError<T extends GraphVertex = GraphVertex>(
cyclePath: CycleDetectionResult<T>['path']
) {
const cycleString = cyclePath.join(' -> ');
return `Circular dependency detected while setting up services: ${cycleString}`;
}

private static throwCyclicPathError(error: string) {
throw new Error(error);
}
}
47 changes: 47 additions & 0 deletions src/plugins/custom_integrations/public/services/create/factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { BehaviorSubject } from 'rxjs';
import { CoreStart, AppUpdater, PluginInitializerContext } from 'src/core/public';

/**
* A factory function for creating a service.
*
* The `Service` generic determines the shape of the API being produced.
* The `StartParameters` generic determines what parameters are expected to
* create the service.
*/
export type PluginServiceFactory<Service, Parameters = {}, RequiredServices = {}> = (
params: Parameters,
requiredServices: RequiredServices
) => Service;

/**
* Parameters necessary to create a Kibana-based service, (e.g. during Plugin
* startup or setup).
*
* The `Start` generic refers to the specific Plugin `TPluginsStart`.
*/
export interface KibanaPluginServiceParams<Start extends {}> {
coreStart: CoreStart;
startPlugins: Start;
appUpdater?: BehaviorSubject<AppUpdater>;
initContext?: PluginInitializerContext;
}

/**
* A factory function for creating a Kibana-based service.
*
* The `Service` generic determines the shape of the API being produced.
* The `Setup` generic refers to the specific Plugin `TPluginsSetup`.
* The `Start` generic refers to the specific Plugin `TPluginsStart`.
*/
export type KibanaPluginServiceFactory<Service, Start extends {}, RequiredServices = {}> = (
params: KibanaPluginServiceParams<Start>,
requiredServices: RequiredServices
) => Service;
97 changes: 97 additions & 0 deletions src/plugins/custom_integrations/public/services/create/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { PluginServiceRegistry } from './registry';

export { PluginServiceRegistry } from './registry';
export type { PluginServiceProviders } from './provider';
export { PluginServiceProvider } from './provider';
export type {
PluginServiceFactory,
KibanaPluginServiceFactory,
KibanaPluginServiceParams,
} from './factory';

type ServiceHooks<Services> = { [K in keyof Services]: { useService: () => Services[K] } };

/**
* `PluginServices` is a top-level class for specifying and accessing services within a plugin.
*
* A `PluginServices` object can be provided with a `PluginServiceRegistry` at any time, which will
* then be used to provide services to any component that accesses it.
*
* The `Services` generic determines the shape of all service APIs being produced.
*/
export class PluginServices<Services> {
private registry: PluginServiceRegistry<Services, any> | null = null;

/**
* Supply a `PluginServiceRegistry` for the class to use to provide services and context.
*
* @param registry A setup and started `PluginServiceRegistry`.
*/
setRegistry(registry: PluginServiceRegistry<Services, any> | null) {
if (registry && !registry.isStarted()) {
throw new Error('Registry has not been started.');
}

this.registry = registry;
}

/**
* Returns true if a registry has been provided, false otherwise.
*/
hasRegistry() {
return !!this.registry;
}

/**
* Private getter that will enforce proper setup throughout the class.
*/
private getRegistry() {
if (!this.registry) {
throw new Error('No registry has been provided.');
}

return this.registry;
}

/**
* Return the React Context Provider that will supply services.
*/
getContextProvider() {
return this.getRegistry().getContextProvider();
}

/**
* Return a map of React Hooks that can be used in React components.
*/
getHooks(): ServiceHooks<Services> {
const registry = this.getRegistry();
const providers = registry.getServiceProviders();

const providerNames = Object.keys(providers) as Array<keyof typeof providers>;

return providerNames.reduce((acc, providerName) => {
acc[providerName] = { useService: providers[providerName].getServiceReactHook() };
return acc;
}, {} as ServiceHooks<Services>);
}

getServices(): Services {
const registry = this.getRegistry();
const providers = registry.getServiceProviders();

const providerNames = Object.keys(providers) as Array<keyof typeof providers>;

return providerNames.reduce((acc, providerName) => {
acc[providerName] = providers[providerName].getService();
return acc;
}, {} as Services);
}
}
Loading