From c45f61cfab396d57f093a84e6e03423a9174a445 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Wed, 5 Jan 2022 14:12:00 +0100 Subject: [PATCH 1/6] fixing potential circular dependencies --- src/plugins/custom_integrations/kibana.json | 9 +++------ src/plugins/custom_integrations/public/types.ts | 7 ++----- src/plugins/data/kibana.json | 2 +- src/plugins/home/kibana.json | 2 +- src/plugins/home/public/plugin.ts | 8 ++++---- src/plugins/home/tsconfig.json | 2 +- x-pack/plugins/security/kibana.json | 2 +- .../public/management/roles/roles_management_app.tsx | 4 ++-- x-pack/plugins/security/public/plugin.test.tsx | 8 ++++---- x-pack/plugins/security/public/plugin.tsx | 4 ++-- x-pack/plugins/security/tsconfig.json | 2 +- 11 files changed, 22 insertions(+), 28 deletions(-) diff --git a/src/plugins/custom_integrations/kibana.json b/src/plugins/custom_integrations/kibana.json index cd58c1aec1ecb..7c11f47f4d82a 100755 --- a/src/plugins/custom_integrations/kibana.json +++ b/src/plugins/custom_integrations/kibana.json @@ -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": ["presentationUtil"], "optionalPlugins": [] } diff --git a/src/plugins/custom_integrations/public/types.ts b/src/plugins/custom_integrations/public/types.ts index 946115329e2b5..d4123cd2f1e13 100755 --- a/src/plugins/custom_integrations/public/types.ts +++ b/src/plugins/custom_integrations/public/types.ts @@ -6,8 +6,6 @@ * Side Public License, v 1. */ -import type { PresentationUtilPluginStart } from '../../presentation_util/public'; - import { CustomIntegration } from '../common'; export interface CustomIntegrationsSetup { @@ -19,6 +17,5 @@ export interface CustomIntegrationsStart { ContextProvider: React.FC; } -export interface CustomIntegrationsStartDependencies { - presentationUtil: PresentationUtilPluginStart; -} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface CustomIntegrationsStartDependencies {} diff --git a/src/plugins/data/kibana.json b/src/plugins/data/kibana.json index 3d70d138d80ed..fad8f82dfe84f 100644 --- a/src/plugins/data/kibana.json +++ b/src/plugins/data/kibana.json @@ -5,7 +5,7 @@ "ui": true, "requiredPlugins": ["bfetch", "expressions", "uiActions", "share", "inspector", "fieldFormats", "dataViews"], "serviceFolders": ["search", "query", "autocomplete", "ui"], - "optionalPlugins": ["usageCollection"], + "optionalPlugins": ["usageCollection", "taskManager"], "extraPublicDirs": ["common"], "requiredBundles": ["kibanaUtils", "kibanaReact", "inspector"], "owner": { diff --git a/src/plugins/home/kibana.json b/src/plugins/home/kibana.json index 3f1916f3142ff..d8c09ab5e80c6 100644 --- a/src/plugins/home/kibana.json +++ b/src/plugins/home/kibana.json @@ -7,7 +7,7 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["data", "share", "urlForwarding"], + "requiredPlugins": ["dataViews", "share", "urlForwarding"], "optionalPlugins": ["usageCollection", "telemetry", "customIntegrations"], "requiredBundles": ["kibanaReact"] } diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts index ac680c78f31eb..187f8fc322c16 100644 --- a/src/plugins/home/public/plugin.ts +++ b/src/plugins/home/public/plugin.ts @@ -28,7 +28,7 @@ import { } from './services'; import { ConfigSchema } from '../config'; import { setServices } from './application/kibana_services'; -import { DataPublicPluginStart } from '../../data/public'; +import { DataViewsPublicPluginStart } from '../../data_views/public'; import { TelemetryPluginStart } from '../../telemetry/public'; import { UsageCollectionSetup } from '../../usage_collection/public'; import { UrlForwardingSetup, UrlForwardingStart } from '../../url_forwarding/public'; @@ -37,7 +37,7 @@ import { PLUGIN_ID, HOME_APP_BASE_PATH } from '../common/constants'; import { SharePluginSetup } from '../../share/public'; export interface HomePluginStartDependencies { - data: DataPublicPluginStart; + dataViews: DataViewsPublicPluginStart; telemetry?: TelemetryPluginStart; urlForwarding: UrlForwardingStart; } @@ -76,7 +76,7 @@ export class HomePublicPlugin const trackUiMetric = usageCollection ? usageCollection.reportUiCounter.bind(usageCollection, 'Kibana_home') : () => {}; - const [coreStart, { telemetry, data, urlForwarding: urlForwardingStart }] = + const [coreStart, { telemetry, dataViews, urlForwarding: urlForwardingStart }] = await core.getStartServices(); setServices({ share, @@ -93,7 +93,7 @@ export class HomePublicPlugin uiSettings: core.uiSettings, addBasePath: core.http.basePath.prepend, getBasePath: core.http.basePath.get, - indexPatternService: data.indexPatterns, + indexPatternService: dataViews, environmentService: this.environmentService, urlForwarding: urlForwardingStart, homeConfig: this.initializerContext.config.get(), diff --git a/src/plugins/home/tsconfig.json b/src/plugins/home/tsconfig.json index fa98b98ff8e1c..4f147a6f8bc96 100644 --- a/src/plugins/home/tsconfig.json +++ b/src/plugins/home/tsconfig.json @@ -10,7 +10,7 @@ "include": ["common/**/*", "public/**/*", "server/**/*", "config.ts"], "references": [ { "path": "../../core/tsconfig.json" }, - { "path": "../data/tsconfig.json" }, + { "path": "../data_views/tsconfig.json" }, { "path": "../custom_integrations/tsconfig.json" }, { "path": "../kibana_react/tsconfig.json" }, { "path": "../share/tsconfig.json" }, diff --git a/x-pack/plugins/security/kibana.json b/x-pack/plugins/security/kibana.json index 2eeac40e22f14..3d0bd9cbcbedc 100644 --- a/x-pack/plugins/security/kibana.json +++ b/x-pack/plugins/security/kibana.json @@ -8,7 +8,7 @@ "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "security"], - "requiredPlugins": ["data", "features", "licensing", "taskManager"], + "requiredPlugins": ["dataViews", "features", "licensing", "taskManager"], "optionalPlugins": ["home", "management", "usageCollection", "spaces", "share"], "server": true, "ui": true, diff --git a/x-pack/plugins/security/public/management/roles/roles_management_app.tsx b/x-pack/plugins/security/public/management/roles/roles_management_app.tsx index fb68fa7857668..62b62c4bf93f7 100644 --- a/x-pack/plugins/security/public/management/roles/roles_management_app.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_management_app.tsx @@ -44,7 +44,7 @@ export const rolesManagementApp = Object.freeze({ title, async mount({ element, theme$, setBreadcrumbs, history }) { const [ - [startServices, { data, features, spaces }], + [startServices, { dataViews, features, spaces }], { RolesGridPage }, { EditRolePage }, { RolesAPIClient }, @@ -108,7 +108,7 @@ export const rolesManagementApp = Object.freeze({ license={license} docLinks={docLinks} uiCapabilities={application.capabilities} - indexPatterns={data.indexPatterns} + indexPatterns={dataViews} history={history} spacesApiUi={spacesApiUi} /> diff --git a/x-pack/plugins/security/public/plugin.test.tsx b/x-pack/plugins/security/public/plugin.test.tsx index 2bc4932b12a0b..98d0ea0ab25a2 100644 --- a/x-pack/plugins/security/public/plugin.test.tsx +++ b/x-pack/plugins/security/public/plugin.test.tsx @@ -10,7 +10,7 @@ import { Observable } from 'rxjs'; import type { CoreSetup } from 'src/core/public'; import { coreMock } from 'src/core/public/mocks'; -import type { DataPublicPluginStart } from 'src/plugins/data/public'; +import type { DataViewsPublicPluginStart } from 'src/plugins/data_views/public'; import { managementPluginMock } from 'src/plugins/management/public/mocks'; import type { FeaturesPluginStart } from '../../features/public'; @@ -92,7 +92,7 @@ describe('Security Plugin', () => { expect( plugin.start(coreMock.createStart({ basePath: '/some-base-path' }), { - data: {} as DataPublicPluginStart, + dataViews: {} as DataViewsPublicPluginStart, features: {} as FeaturesPluginStart, }) ).toEqual({ @@ -133,7 +133,7 @@ describe('Security Plugin', () => { const coreStart = coreMock.createStart({ basePath: '/some-base-path' }); plugin.start(coreStart, { - data: {} as DataPublicPluginStart, + dataViews: {} as DataViewsPublicPluginStart, features: {} as FeaturesPluginStart, management: managementStartMock, }); @@ -162,7 +162,7 @@ describe('Security Plugin', () => { ); plugin.start(coreMock.createStart({ basePath: '/some-base-path' }), { - data: {} as DataPublicPluginStart, + dataViews: {} as DataViewsPublicPluginStart, features: {} as FeaturesPluginStart, }); diff --git a/x-pack/plugins/security/public/plugin.tsx b/x-pack/plugins/security/public/plugin.tsx index c2860ec059b8d..02618bbc7977a 100644 --- a/x-pack/plugins/security/public/plugin.tsx +++ b/x-pack/plugins/security/public/plugin.tsx @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; -import type { DataPublicPluginStart } from 'src/plugins/data/public'; +import type { DataViewsPublicPluginStart } from 'src/plugins/data_views/public'; import type { HomePublicPluginSetup } from 'src/plugins/home/public'; import type { ManagementSetup, ManagementStart } from 'src/plugins/management/public'; @@ -39,7 +39,7 @@ export interface PluginSetupDependencies { } export interface PluginStartDependencies { - data: DataPublicPluginStart; + dataViews: DataViewsPublicPluginStart; features: FeaturesPluginStart; management?: ManagementStart; spaces?: SpacesPluginStart; diff --git a/x-pack/plugins/security/tsconfig.json b/x-pack/plugins/security/tsconfig.json index 5cc25bbb44055..e4566248efc46 100644 --- a/x-pack/plugins/security/tsconfig.json +++ b/x-pack/plugins/security/tsconfig.json @@ -12,7 +12,7 @@ { "path": "../licensing/tsconfig.json" }, { "path": "../spaces/tsconfig.json" }, { "path": "../task_manager/tsconfig.json" }, - { "path": "../../../src/plugins/data/tsconfig.json" }, + { "path": "../../../src/plugins/data_views/tsconfig.json" }, { "path": "../../../src/plugins/es_ui_shared/tsconfig.json" }, { "path": "../../../src/plugins/home/tsconfig.json" }, { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, From df7151a8898635b851642c5fc1bebce9cce34f89 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 6 Jan 2022 15:42:31 +0100 Subject: [PATCH 2/6] add security as optional deps --- src/plugins/data/kibana.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/data/kibana.json b/src/plugins/data/kibana.json index fad8f82dfe84f..e3369c2d571a6 100644 --- a/src/plugins/data/kibana.json +++ b/src/plugins/data/kibana.json @@ -5,7 +5,7 @@ "ui": true, "requiredPlugins": ["bfetch", "expressions", "uiActions", "share", "inspector", "fieldFormats", "dataViews"], "serviceFolders": ["search", "query", "autocomplete", "ui"], - "optionalPlugins": ["usageCollection", "taskManager"], + "optionalPlugins": ["usageCollection", "taskManager", "security"], "extraPublicDirs": ["common"], "requiredBundles": ["kibanaUtils", "kibanaReact", "inspector"], "owner": { From e13aa8820498a24b0a83a5fa44328aacbd64c67a Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Wed, 23 Mar 2022 16:22:12 +0100 Subject: [PATCH 3/6] revert adding security just to check if still works --- src/plugins/data/kibana.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/data/kibana.json b/src/plugins/data/kibana.json index e3369c2d571a6..fad8f82dfe84f 100644 --- a/src/plugins/data/kibana.json +++ b/src/plugins/data/kibana.json @@ -5,7 +5,7 @@ "ui": true, "requiredPlugins": ["bfetch", "expressions", "uiActions", "share", "inspector", "fieldFormats", "dataViews"], "serviceFolders": ["search", "query", "autocomplete", "ui"], - "optionalPlugins": ["usageCollection", "taskManager", "security"], + "optionalPlugins": ["usageCollection", "taskManager"], "extraPublicDirs": ["common"], "requiredBundles": ["kibanaUtils", "kibanaReact", "inspector"], "owner": { From 4e88d58b57f883440c2f92ec00fa08a5645261a9 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 24 Mar 2022 11:57:52 +0100 Subject: [PATCH 4/6] add security back --- src/plugins/data/kibana.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/data/kibana.json b/src/plugins/data/kibana.json index fad8f82dfe84f..88abad8e3e0e1 100644 --- a/src/plugins/data/kibana.json +++ b/src/plugins/data/kibana.json @@ -3,7 +3,7 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["bfetch", "expressions", "uiActions", "share", "inspector", "fieldFormats", "dataViews"], + "requiredPlugins": ["bfetch", "expressions", "uiActions", "share", "inspector", "fieldFormats", "dataViews", "security"], "serviceFolders": ["search", "query", "autocomplete", "ui"], "optionalPlugins": ["usageCollection", "taskManager"], "extraPublicDirs": ["common"], From 897187f61e42cf876505f8a2f52d70504c3766d7 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 24 Mar 2022 11:58:55 +0100 Subject: [PATCH 5/6] add security back --- src/plugins/data/kibana.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/data/kibana.json b/src/plugins/data/kibana.json index 88abad8e3e0e1..e3369c2d571a6 100644 --- a/src/plugins/data/kibana.json +++ b/src/plugins/data/kibana.json @@ -3,9 +3,9 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["bfetch", "expressions", "uiActions", "share", "inspector", "fieldFormats", "dataViews", "security"], + "requiredPlugins": ["bfetch", "expressions", "uiActions", "share", "inspector", "fieldFormats", "dataViews"], "serviceFolders": ["search", "query", "autocomplete", "ui"], - "optionalPlugins": ["usageCollection", "taskManager"], + "optionalPlugins": ["usageCollection", "taskManager", "security"], "extraPublicDirs": ["common"], "requiredBundles": ["kibanaUtils", "kibanaReact", "inspector"], "owner": { From 08ba9dc6f62f81d3f0e60bc26131fd3fc1c30ac1 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 24 Mar 2022 12:21:46 +0100 Subject: [PATCH 6/6] remove custom_integration deps on presentation utils --- src/plugins/custom_integrations/kibana.json | 2 +- .../custom_integrations/public/mocks.ts | 2 +- .../create/dependency_manager.test.ts | 77 +++++++++++ .../services/create/dependency_manager.ts | 122 +++++++++++++++++ .../public/services/create/factory.ts | 47 +++++++ .../public/services/create/index.ts | 97 ++++++++++++++ .../public/services/create/provider.tsx | 126 ++++++++++++++++++ .../services/create/providers_mediator.ts | 55 ++++++++ .../public/services/create/registry.tsx | 88 ++++++++++++ .../public/services/index.ts | 2 +- .../public/services/kibana/find.ts | 2 +- .../public/services/kibana/index.ts | 2 +- .../public/services/kibana/platform.ts | 2 +- .../public/services/storybook/index.ts | 6 +- .../public/services/stub/find.ts | 2 +- .../public/services/stub/index.ts | 6 +- .../public/services/stub/platform.ts | 2 +- .../storybook/decorator.tsx | 2 +- src/plugins/custom_integrations/tsconfig.json | 2 +- 19 files changed, 624 insertions(+), 20 deletions(-) create mode 100644 src/plugins/custom_integrations/public/services/create/dependency_manager.test.ts create mode 100644 src/plugins/custom_integrations/public/services/create/dependency_manager.ts create mode 100644 src/plugins/custom_integrations/public/services/create/factory.ts create mode 100644 src/plugins/custom_integrations/public/services/create/index.ts create mode 100644 src/plugins/custom_integrations/public/services/create/provider.tsx create mode 100644 src/plugins/custom_integrations/public/services/create/providers_mediator.ts create mode 100644 src/plugins/custom_integrations/public/services/create/registry.tsx diff --git a/src/plugins/custom_integrations/kibana.json b/src/plugins/custom_integrations/kibana.json index 7c11f47f4d82a..d9a7618b25642 100755 --- a/src/plugins/custom_integrations/kibana.json +++ b/src/plugins/custom_integrations/kibana.json @@ -11,6 +11,6 @@ "server": true, "extraPublicDirs": ["common"], "requiredPlugins": [], - "requiredBundles": ["presentationUtil"], + "requiredBundles": [], "optionalPlugins": [] } diff --git a/src/plugins/custom_integrations/public/mocks.ts b/src/plugins/custom_integrations/public/mocks.ts index a8fedbbb712b2..ac8178fc5420f 100644 --- a/src/plugins/custom_integrations/public/mocks.ts +++ b/src/plugins/custom_integrations/public/mocks.ts @@ -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'; diff --git a/src/plugins/custom_integrations/public/services/create/dependency_manager.test.ts b/src/plugins/custom_integrations/public/services/create/dependency_manager.test.ts new file mode 100644 index 0000000000000..8e67dee3f8b6b --- /dev/null +++ b/src/plugins/custom_integrations/public/services/create/dependency_manager.test.ts @@ -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(); + }); + }); +}); diff --git a/src/plugins/custom_integrations/public/services/create/dependency_manager.ts b/src/plugins/custom_integrations/public/services/create/dependency_manager.ts new file mode 100644 index 0000000000000..3925f3e9d9c4f --- /dev/null +++ b/src/plugins/custom_integrations/public/services/create/dependency_manager.ts @@ -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 = Record; +type BreadCrumbs = Record; + +interface CycleDetectionResult { + hasCycle: boolean; + path: T[]; +} + +export class DependencyManager { + static orderDependencies(graph: Graph) { + 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( + graph: Graph = {} as Graph + ): CycleDetectionResult { + const sortedVertices: Set = new Set(); + const vertices = Object.keys(graph) as T[]; + return vertices.reduce>((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} graph - graph of dependencies, represented in the adjacency list form. + * @param {Set} 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( + srcVertex: T, + graph: Graph, + sortedVertices: Set, + visited: BreadCrumbs = {}, + inpath: BreadCrumbs = {}, + cycle: CycleDetectionResult + ): CycleDetectionResult { + visited[srcVertex] = true; + inpath[srcVertex] = true; + + const vertexEdges = + graph[srcVertex] === undefined || graph[srcVertex] === null ? [] : graph[srcVertex]; + + cycle = vertexEdges!.reduce>((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( + path: T[] = [], + hasCycle: boolean = false + ): CycleDetectionResult { + return { hasCycle, path }; + } + + private static getCyclePathError( + cyclePath: CycleDetectionResult['path'] + ) { + const cycleString = cyclePath.join(' -> '); + return `Circular dependency detected while setting up services: ${cycleString}`; + } + + private static throwCyclicPathError(error: string) { + throw new Error(error); + } +} diff --git a/src/plugins/custom_integrations/public/services/create/factory.ts b/src/plugins/custom_integrations/public/services/create/factory.ts new file mode 100644 index 0000000000000..49ed5ef8aaf8d --- /dev/null +++ b/src/plugins/custom_integrations/public/services/create/factory.ts @@ -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 = ( + 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 { + coreStart: CoreStart; + startPlugins: Start; + appUpdater?: BehaviorSubject; + 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 = ( + params: KibanaPluginServiceParams, + requiredServices: RequiredServices +) => Service; diff --git a/src/plugins/custom_integrations/public/services/create/index.ts b/src/plugins/custom_integrations/public/services/create/index.ts new file mode 100644 index 0000000000000..d616d7bee20c8 --- /dev/null +++ b/src/plugins/custom_integrations/public/services/create/index.ts @@ -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 = { [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 { + private registry: PluginServiceRegistry | 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 | 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 { + const registry = this.getRegistry(); + const providers = registry.getServiceProviders(); + + const providerNames = Object.keys(providers) as Array; + + return providerNames.reduce((acc, providerName) => { + acc[providerName] = { useService: providers[providerName].getServiceReactHook() }; + return acc; + }, {} as ServiceHooks); + } + + getServices(): Services { + const registry = this.getRegistry(); + const providers = registry.getServiceProviders(); + + const providerNames = Object.keys(providers) as Array; + + return providerNames.reduce((acc, providerName) => { + acc[providerName] = providers[providerName].getService(); + return acc; + }, {} as Services); + } +} diff --git a/src/plugins/custom_integrations/public/services/create/provider.tsx b/src/plugins/custom_integrations/public/services/create/provider.tsx new file mode 100644 index 0000000000000..3271dc52fd9d0 --- /dev/null +++ b/src/plugins/custom_integrations/public/services/create/provider.tsx @@ -0,0 +1,126 @@ +/* + * 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 React, { createContext, useContext } from 'react'; +import { PluginServiceFactory } from './factory'; + +/** + * A collection of `PluginServiceProvider` objects, keyed by the `Services` API generic. + * + * The `Services` generic determines the shape of all service APIs being produced. + * The `StartParameters` generic determines what parameters are expected to + * start the service. + */ +export type PluginServiceProviders = { + [K in keyof Services]: PluginServiceProvider< + Services[K], + StartParameters, + Services, + Array + >; +}; + +type ElementOfArray = ArrayType extends Array< + infer ElementType +> + ? ElementType + : never; + +export type PluginServiceRequiredServices< + RequiredServices extends Array, + AvailableServices +> = { + [K in ElementOfArray]: AvailableServices[K]; +}; + +/** + * An object which uses a given factory to start, stop or provide a service. + * + * The `Service` generic determines the shape of the API being produced. + * The `StartParameters` generic determines what parameters are expected to + * start the service. + */ +export class PluginServiceProvider< + Service extends {}, + StartParameters = {}, + Services = {}, + RequiredServices extends Array = [] +> { + private factory: PluginServiceFactory< + Service, + StartParameters, + PluginServiceRequiredServices + >; + private _requiredServices?: RequiredServices; + private context = createContext(null); + private pluginService: Service | null = null; + public readonly Provider: React.FC = ({ children }) => { + return {children}; + }; + + constructor( + factory: PluginServiceFactory< + Service, + StartParameters, + PluginServiceRequiredServices + >, + requiredServices?: RequiredServices + ) { + this.factory = factory; + this._requiredServices = requiredServices; + this.context.displayName = 'PluginServiceContext'; + } + + /** + * Getter that will enforce proper setup throughout the class. + */ + public getService() { + if (!this.pluginService) { + throw new Error('Service not started'); + } + return this.pluginService; + } + + /** + * Start the service. + * + * @param params Parameters used to start the service. + */ + start( + params: StartParameters, + requiredServices: PluginServiceRequiredServices + ) { + this.pluginService = this.factory(params, requiredServices); + } + + /** + * Returns a function for providing a Context hook for the service. + */ + getServiceReactHook() { + return () => { + const service = useContext(this.context); + + if (!service) { + throw new Error('Provider is not set up correctly'); + } + + return service; + }; + } + + /** + * Stop the service. + */ + stop() { + this.pluginService = null; + } + + public get requiredServices() { + return this._requiredServices ?? []; + } +} diff --git a/src/plugins/custom_integrations/public/services/create/providers_mediator.ts b/src/plugins/custom_integrations/public/services/create/providers_mediator.ts new file mode 100644 index 0000000000000..dd5937149850c --- /dev/null +++ b/src/plugins/custom_integrations/public/services/create/providers_mediator.ts @@ -0,0 +1,55 @@ +/* + * 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'; +import { PluginServiceProviders, PluginServiceRequiredServices } from './provider'; + +export class PluginServiceProvidersMediator { + constructor(private readonly providers: PluginServiceProviders) {} + + start(params: StartParameters) { + this.getOrderedDependencies().forEach((service) => { + this.providers[service].start(params, this.getServiceDependencies(service)); + }); + } + + stop() { + this.getOrderedDependencies().forEach((service) => this.providers[service].stop()); + } + + private getOrderedDependencies() { + const dependenciesGraph = this.getGraphOfDependencies(); + return DependencyManager.orderDependencies(dependenciesGraph); + } + + private getGraphOfDependencies() { + return this.getProvidersNames().reduce>>( + (graph, vertex) => ({ ...graph, [vertex]: this.providers[vertex].requiredServices ?? [] }), + {} as Record> + ); + } + + private getProvidersNames() { + return Object.keys(this.providers) as Array; + } + + private getServiceDependencies(service: keyof Services) { + const requiredServices = this.providers[service].requiredServices ?? []; + return this.getServicesByDeps(requiredServices); + } + + private getServicesByDeps(deps: Array) { + return deps.reduce, Services>>( + (services, dependency) => ({ + ...services, + [dependency]: this.providers[dependency].getService(), + }), + {} as PluginServiceRequiredServices, Services> + ); + } +} diff --git a/src/plugins/custom_integrations/public/services/create/registry.tsx b/src/plugins/custom_integrations/public/services/create/registry.tsx new file mode 100644 index 0000000000000..8369815a042af --- /dev/null +++ b/src/plugins/custom_integrations/public/services/create/registry.tsx @@ -0,0 +1,88 @@ +/* + * 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 React from 'react'; +import { PluginServiceProvider, PluginServiceProviders } from './provider'; +import { PluginServiceProvidersMediator } from './providers_mediator'; + +/** + * A `PluginServiceRegistry` maintains a set of service providers which can be collectively + * started, stopped or retreived. + * + * The `Services` generic determines the shape of all service APIs being produced. + * The `StartParameters` generic determines what parameters are expected to + * start the service. + */ +export class PluginServiceRegistry { + private providers: PluginServiceProviders; + private providersMediator: PluginServiceProvidersMediator; + private _isStarted = false; + + constructor(providers: PluginServiceProviders) { + this.providers = providers; + this.providersMediator = new PluginServiceProvidersMediator(providers); + } + + /** + * Returns true if the registry has been started, false otherwise. + */ + isStarted() { + return this._isStarted; + } + + /** + * Returns a map of `PluginServiceProvider` objects. + */ + getServiceProviders() { + if (!this._isStarted) { + throw new Error('Registry not started'); + } + return this.providers; + } + + /** + * Returns a React Context Provider for use in consuming applications. + */ + getContextProvider() { + const values = Object.values(this.getServiceProviders()) as Array< + PluginServiceProvider + >; + + // Collect and combine Context.Provider elements from each Service Provider into a single + // Functional Component. + const provider: React.FC = ({ children }) => ( + <> + {values.reduceRight((acc, serviceProvider) => { + return {acc}; + }, children)} + + ); + + return provider; + } + + /** + * Start the registry. + * + * @param params Parameters used to start the registry. + */ + start(params: StartParameters) { + this.providersMediator.start(params); + this._isStarted = true; + return this; + } + + /** + * Stop the registry. + */ + stop() { + this.providersMediator.stop(); + this._isStarted = false; + return this; + } +} diff --git a/src/plugins/custom_integrations/public/services/index.ts b/src/plugins/custom_integrations/public/services/index.ts index 8a257ee1a2cd7..bc924b027003b 100644 --- a/src/plugins/custom_integrations/public/services/index.ts +++ b/src/plugins/custom_integrations/public/services/index.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { PluginServices } from '../../../presentation_util/public'; +import { PluginServices } from './create'; import { CustomIntegrationsFindService } from './find'; import { CustomIntegrationsPlatformService } from './platform'; diff --git a/src/plugins/custom_integrations/public/services/kibana/find.ts b/src/plugins/custom_integrations/public/services/kibana/find.ts index 5fc7626baa1e1..b02d5ce4e2580 100644 --- a/src/plugins/custom_integrations/public/services/kibana/find.ts +++ b/src/plugins/custom_integrations/public/services/kibana/find.ts @@ -11,7 +11,7 @@ import { ROUTES_APPEND_CUSTOM_INTEGRATIONS, ROUTES_REPLACEMENT_CUSTOM_INTEGRATIONS, } from '../../../common'; -import { KibanaPluginServiceFactory } from '../../../../presentation_util/public'; +import { KibanaPluginServiceFactory } from '../create'; import { CustomIntegrationsStartDependencies } from '../../types'; import { CustomIntegrationsFindService, filterCustomIntegrations } from '../find'; diff --git a/src/plugins/custom_integrations/public/services/kibana/index.ts b/src/plugins/custom_integrations/public/services/kibana/index.ts index d3cf27b9bc7c0..0b0aa10eb033a 100644 --- a/src/plugins/custom_integrations/public/services/kibana/index.ts +++ b/src/plugins/custom_integrations/public/services/kibana/index.ts @@ -11,7 +11,7 @@ import { PluginServiceProvider, PluginServiceRegistry, KibanaPluginServiceParams, -} from '../../../../presentation_util/public'; +} from '../create'; import { CustomIntegrationsServices } from '..'; import { CustomIntegrationsStartDependencies } from '../../types'; diff --git a/src/plugins/custom_integrations/public/services/kibana/platform.ts b/src/plugins/custom_integrations/public/services/kibana/platform.ts index e6fe89b68c975..efb5a01b6470e 100644 --- a/src/plugins/custom_integrations/public/services/kibana/platform.ts +++ b/src/plugins/custom_integrations/public/services/kibana/platform.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { KibanaPluginServiceFactory } from '../../../../presentation_util/public'; +import { KibanaPluginServiceFactory } from '../create'; import type { CustomIntegrationsPlatformService } from '../platform'; import type { CustomIntegrationsStartDependencies } from '../../types'; diff --git a/src/plugins/custom_integrations/public/services/storybook/index.ts b/src/plugins/custom_integrations/public/services/storybook/index.ts index 4dfed1b37e294..8d9c9240a97ed 100644 --- a/src/plugins/custom_integrations/public/services/storybook/index.ts +++ b/src/plugins/custom_integrations/public/services/storybook/index.ts @@ -6,11 +6,7 @@ * Side Public License, v 1. */ -import { - PluginServiceProviders, - PluginServiceProvider, - PluginServiceRegistry, -} from '../../../../presentation_util/public'; +import { PluginServiceProviders, PluginServiceProvider, PluginServiceRegistry } from '../create'; import { CustomIntegrationsServices } from '..'; import { findServiceFactory } from '../stub/find'; diff --git a/src/plugins/custom_integrations/public/services/stub/find.ts b/src/plugins/custom_integrations/public/services/stub/find.ts index 08def4e63471d..b10c1636dffd1 100644 --- a/src/plugins/custom_integrations/public/services/stub/find.ts +++ b/src/plugins/custom_integrations/public/services/stub/find.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { PluginServiceFactory } from '../../../../presentation_util/public'; +import { PluginServiceFactory } from '../create'; import { CustomIntegrationsFindService, filterCustomIntegrations } from '../find'; diff --git a/src/plugins/custom_integrations/public/services/stub/index.ts b/src/plugins/custom_integrations/public/services/stub/index.ts index fe7465949d565..5e944c6262bb5 100644 --- a/src/plugins/custom_integrations/public/services/stub/index.ts +++ b/src/plugins/custom_integrations/public/services/stub/index.ts @@ -6,11 +6,7 @@ * Side Public License, v 1. */ -import { - PluginServiceProviders, - PluginServiceProvider, - PluginServiceRegistry, -} from '../../../../presentation_util/public'; +import { PluginServiceProviders, PluginServiceProvider, PluginServiceRegistry } from '../create'; import { CustomIntegrationsServices } from '..'; import { findServiceFactory } from './find'; diff --git a/src/plugins/custom_integrations/public/services/stub/platform.ts b/src/plugins/custom_integrations/public/services/stub/platform.ts index 81891c0c3ac40..a7fc53530ef53 100644 --- a/src/plugins/custom_integrations/public/services/stub/platform.ts +++ b/src/plugins/custom_integrations/public/services/stub/platform.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { PluginServiceFactory } from '../../../../presentation_util/public'; +import { PluginServiceFactory } from '../create'; import type { CustomIntegrationsPlatformService } from '../platform'; diff --git a/src/plugins/custom_integrations/storybook/decorator.tsx b/src/plugins/custom_integrations/storybook/decorator.tsx index eff12dde9b74a..44daf85ec3b51 100644 --- a/src/plugins/custom_integrations/storybook/decorator.tsx +++ b/src/plugins/custom_integrations/storybook/decorator.tsx @@ -11,7 +11,7 @@ import React from 'react'; import { DecoratorFn } from '@storybook/react'; import { I18nProvider } from '@kbn/i18n-react'; -import { PluginServiceRegistry } from '../../presentation_util/public'; +import { PluginServiceRegistry } from '../public/services/create'; import { pluginServices } from '../public/services'; import { CustomIntegrationsServices } from '../public/services'; diff --git a/src/plugins/custom_integrations/tsconfig.json b/src/plugins/custom_integrations/tsconfig.json index ccb75c358611b..f24c82ef1c62c 100644 --- a/src/plugins/custom_integrations/tsconfig.json +++ b/src/plugins/custom_integrations/tsconfig.json @@ -15,6 +15,6 @@ ], "references": [ { "path": "../../core/tsconfig.json" }, - { "path": "../presentation_util/tsconfig.json" } + { "path": "../../plugins/kibana_react/tsconfig.json" } ] }