From bd291cc5594924907c11c8922f2ec35ed66d51d4 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Mon, 28 Oct 2019 13:08:30 -0500 Subject: [PATCH] Migrate ui/registry/feature_catalogue to New Platform plugin (#48818) --- .../core_plugins/kibana/public/home/index.js | 2 +- .../kibana/public/home/kibana_services.ts | 10 ++- .../ui/public/new_platform/new_platform.ts | 6 ++ .../ui/public/registry/feature_catalogue.js | 7 +- src/plugins/feature_catalogue/README.md | 26 ++++++ src/plugins/feature_catalogue/kibana.json | 6 ++ src/plugins/feature_catalogue/public/index.ts | 24 +++++ .../public/plugin.test.mocks.ts | 25 ++++++ .../feature_catalogue/public/plugin.test.ts | 49 +++++++++++ .../feature_catalogue/public/plugin.ts | 50 +++++++++++ .../feature_catalogue_registry.mock.ts | 54 ++++++++++++ .../feature_catalogue_registry.test.ts | 87 +++++++++++++++++++ .../services/feature_catalogue_registry.ts | 86 ++++++++++++++++++ .../public/services/index.ts | 20 +++++ 14 files changed, 442 insertions(+), 10 deletions(-) create mode 100644 src/plugins/feature_catalogue/README.md create mode 100644 src/plugins/feature_catalogue/kibana.json create mode 100644 src/plugins/feature_catalogue/public/index.ts create mode 100644 src/plugins/feature_catalogue/public/plugin.test.mocks.ts create mode 100644 src/plugins/feature_catalogue/public/plugin.test.ts create mode 100644 src/plugins/feature_catalogue/public/plugin.ts create mode 100644 src/plugins/feature_catalogue/public/services/feature_catalogue_registry.mock.ts create mode 100644 src/plugins/feature_catalogue/public/services/feature_catalogue_registry.test.ts create mode 100644 src/plugins/feature_catalogue/public/services/feature_catalogue_registry.ts create mode 100644 src/plugins/feature_catalogue/public/services/index.ts diff --git a/src/legacy/core_plugins/kibana/public/home/index.js b/src/legacy/core_plugins/kibana/public/home/index.js index 8a8680eeba47c..01f94b8ee4368 100644 --- a/src/legacy/core_plugins/kibana/public/home/index.js +++ b/src/legacy/core_plugins/kibana/public/home/index.js @@ -37,7 +37,7 @@ function getRoute() { return { template, resolve: { - directories: () => getServices().getFeatureCatalogueRegistryProvider().then(catalogue => catalogue.inTitleOrder) + directories: () => getServices().getFeatureCatalogueEntries() }, controller($scope, $route) { const { chrome, addBasePath } = getServices(); diff --git a/src/legacy/core_plugins/kibana/public/home/kibana_services.ts b/src/legacy/core_plugins/kibana/public/home/kibana_services.ts index 39067e2271f28..b9f2ae1cfa7e8 100644 --- a/src/legacy/core_plugins/kibana/public/home/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/home/kibana_services.ts @@ -27,7 +27,7 @@ import { wrapInI18nContext } from 'ui/i18n'; // @ts-ignore import { uiModules as modules } from 'ui/modules'; import routes from 'ui/routes'; -import { npStart } from 'ui/new_platform'; +import { npSetup, npStart } from 'ui/new_platform'; import { IPrivate } from 'ui/private'; import { FeatureCatalogueRegistryProvider } from 'ui/registry/feature_catalogue'; import { createUiStatsReporter, METRIC_TYPE } from '../../../ui_metric/public'; @@ -55,10 +55,14 @@ export function getServices() { indexPatternService: data.indexPatterns.indexPatterns, shouldShowTelemetryOptIn, telemetryOptInProvider, - getFeatureCatalogueRegistryProvider: async () => { + getFeatureCatalogueEntries: async () => { const injector = await chrome.dangerouslyGetActiveInjector(); const Private = injector.get('Private'); - return Private(FeatureCatalogueRegistryProvider as any); + // Merge legacy registry with new registry + (Private(FeatureCatalogueRegistryProvider as any) as any).inTitleOrder.map( + npSetup.plugins.feature_catalogue.register + ); + return npStart.plugins.feature_catalogue.get(); }, trackUiMetric: createUiStatsReporter('Kibana_home'), diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index b86f9cde0125c..0c7b28e7da3df 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -28,11 +28,16 @@ import { Start as InspectorStart, } from '../../../../plugins/inspector/public'; import { EuiUtilsStart } from '../../../../plugins/eui_utils/public'; +import { + FeatureCatalogueSetup, + FeatureCatalogueStart, +} from '../../../../plugins/feature_catalogue/public'; export interface PluginsSetup { data: ReturnType; embeddable: EmbeddableSetup; expressions: ReturnType; + feature_catalogue: FeatureCatalogueSetup; inspector: InspectorSetup; uiActions: IUiActionsSetup; } @@ -42,6 +47,7 @@ export interface PluginsStart { embeddable: EmbeddableStart; eui_utils: EuiUtilsStart; expressions: ReturnType; + feature_catalogue: FeatureCatalogueStart; inspector: InspectorStart; uiActions: IUiActionsStart; } diff --git a/src/legacy/ui/public/registry/feature_catalogue.js b/src/legacy/ui/public/registry/feature_catalogue.js index 0eb8a4a334521..8905a15106953 100644 --- a/src/legacy/ui/public/registry/feature_catalogue.js +++ b/src/legacy/ui/public/registry/feature_catalogue.js @@ -19,6 +19,7 @@ import { uiRegistry } from './_registry'; import { capabilities } from '../capabilities'; +export { FeatureCatalogueCategory } from '../../../../plugins/feature_catalogue/public'; export const FeatureCatalogueRegistryProvider = uiRegistry({ name: 'featureCatalogue', @@ -30,9 +31,3 @@ export const FeatureCatalogueRegistryProvider = uiRegistry({ return !isDisabledViaCapabilities && Object.keys(featureCatalogItem).length > 0; } }); - -export const FeatureCatalogueCategory = { - ADMIN: 'admin', - DATA: 'data', - OTHER: 'other' -}; diff --git a/src/plugins/feature_catalogue/README.md b/src/plugins/feature_catalogue/README.md new file mode 100644 index 0000000000000..68584e7ed2ce1 --- /dev/null +++ b/src/plugins/feature_catalogue/README.md @@ -0,0 +1,26 @@ +# Feature catalogue plugin + +Replaces the legacy `ui/registry/feature_catalogue` module for registering "features" that should be showed in the home +page's feature catalogue. This should not be confused with the "feature" plugin for registering features used to derive +UI capabilities for feature controls. + +## Example registration + +```ts +// For legacy plugins +import { npSetup } from 'ui/new_platform'; +npSetup.plugins.feature_catalogue.register(/* same details here */); + +// For new plugins: first add 'feature_catalogue` to the list of `optionalPlugins` +// in your kibana.json file. Then access the plugin directly in `setup`: + +class MyPlugin { + setup(core, plugins) { + if (plugins.feature_catalogue) { + plugins.feature_catalogue.register(/* same details here. */); + } + } +} +``` + +Note that the old module supported providing a Angular DI function to receive Angular dependencies. This is no longer supported as we migrate away from Angular and will be removed in 8.0. diff --git a/src/plugins/feature_catalogue/kibana.json b/src/plugins/feature_catalogue/kibana.json new file mode 100644 index 0000000000000..3f39c9361f047 --- /dev/null +++ b/src/plugins/feature_catalogue/kibana.json @@ -0,0 +1,6 @@ +{ + "id": "feature_catalogue", + "version": "kibana", + "server": false, + "ui": true +} diff --git a/src/plugins/feature_catalogue/public/index.ts b/src/plugins/feature_catalogue/public/index.ts new file mode 100644 index 0000000000000..dd241a317c4a6 --- /dev/null +++ b/src/plugins/feature_catalogue/public/index.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { FeatureCatalogueSetup, FeatureCatalogueStart } from './plugin'; +export { FeatureCatalogueEntry, FeatureCatalogueCategory } from './services'; +import { FeatureCataloguePlugin } from './plugin'; + +export const plugin = () => new FeatureCataloguePlugin(); diff --git a/src/plugins/feature_catalogue/public/plugin.test.mocks.ts b/src/plugins/feature_catalogue/public/plugin.test.mocks.ts new file mode 100644 index 0000000000000..c0da6a179204b --- /dev/null +++ b/src/plugins/feature_catalogue/public/plugin.test.mocks.ts @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { featureCatalogueRegistryMock } from './services/feature_catalogue_registry.mock'; + +export const registryMock = featureCatalogueRegistryMock.create(); +jest.doMock('./services', () => ({ + FeatureCatalogueRegistry: jest.fn(() => registryMock), +})); diff --git a/src/plugins/feature_catalogue/public/plugin.test.ts b/src/plugins/feature_catalogue/public/plugin.test.ts new file mode 100644 index 0000000000000..8bbbb973b459e --- /dev/null +++ b/src/plugins/feature_catalogue/public/plugin.test.ts @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { registryMock } from './plugin.test.mocks'; +import { FeatureCataloguePlugin } from './plugin'; + +describe('FeatureCataloguePlugin', () => { + beforeEach(() => { + registryMock.setup.mockClear(); + registryMock.start.mockClear(); + }); + + describe('setup', () => { + test('wires up and returns registry', async () => { + const setup = await new FeatureCataloguePlugin().setup(); + expect(registryMock.setup).toHaveBeenCalledWith(); + expect(setup.register).toBeDefined(); + }); + }); + + describe('start', () => { + test('wires up and returns registry', async () => { + const service = new FeatureCataloguePlugin(); + await service.setup(); + const core = { application: { capabilities: { catalogue: {} } } } as any; + const start = await service.start(core); + expect(registryMock.start).toHaveBeenCalledWith({ + capabilities: core.application.capabilities, + }); + expect(start.get).toBeDefined(); + }); + }); +}); diff --git a/src/plugins/feature_catalogue/public/plugin.ts b/src/plugins/feature_catalogue/public/plugin.ts new file mode 100644 index 0000000000000..46a70baff488a --- /dev/null +++ b/src/plugins/feature_catalogue/public/plugin.ts @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreStart, Plugin } from 'src/core/public'; +import { + FeatureCatalogueRegistry, + FeatureCatalogueRegistrySetup, + FeatureCatalogueRegistryStart, +} from './services'; + +export class FeatureCataloguePlugin + implements Plugin { + private readonly featuresCatalogueRegistry = new FeatureCatalogueRegistry(); + + public async setup() { + return { + ...this.featuresCatalogueRegistry.setup(), + }; + } + + public async start(core: CoreStart) { + return { + ...this.featuresCatalogueRegistry.start({ + capabilities: core.application.capabilities, + }), + }; + } +} + +/** @public */ +export type FeatureCatalogueSetup = FeatureCatalogueRegistrySetup; + +/** @public */ +export type FeatureCatalogueStart = FeatureCatalogueRegistryStart; diff --git a/src/plugins/feature_catalogue/public/services/feature_catalogue_registry.mock.ts b/src/plugins/feature_catalogue/public/services/feature_catalogue_registry.mock.ts new file mode 100644 index 0000000000000..54bdd42c1cca9 --- /dev/null +++ b/src/plugins/feature_catalogue/public/services/feature_catalogue_registry.mock.ts @@ -0,0 +1,54 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + FeatureCatalogueRegistrySetup, + FeatureCatalogueRegistryStart, + FeatureCatalogueRegistry, +} from './feature_catalogue_registry'; + +const createSetupMock = (): jest.Mocked => { + const setup = { + register: jest.fn(), + }; + return setup; +}; + +const createStartMock = (): jest.Mocked => { + const start = { + get: jest.fn(), + }; + return start; +}; + +const createMock = (): jest.Mocked> => { + const service = { + setup: jest.fn(), + start: jest.fn(), + }; + service.setup.mockImplementation(createSetupMock); + service.start.mockImplementation(createStartMock); + return service; +}; + +export const featureCatalogueRegistryMock = { + createSetup: createSetupMock, + createStart: createStartMock, + create: createMock, +}; diff --git a/src/plugins/feature_catalogue/public/services/feature_catalogue_registry.test.ts b/src/plugins/feature_catalogue/public/services/feature_catalogue_registry.test.ts new file mode 100644 index 0000000000000..b174a68aa53be --- /dev/null +++ b/src/plugins/feature_catalogue/public/services/feature_catalogue_registry.test.ts @@ -0,0 +1,87 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + FeatureCatalogueRegistry, + FeatureCatalogueCategory, + FeatureCatalogueEntry, +} from './feature_catalogue_registry'; + +const DASHBOARD_FEATURE: FeatureCatalogueEntry = { + id: 'dashboard', + title: 'Dashboard', + description: 'Display and share a collection of visualizations and saved searches.', + icon: 'dashboardApp', + path: `/app/kibana#dashboard`, + showOnHomePage: true, + category: FeatureCatalogueCategory.DATA, +}; + +describe('FeatureCatalogueRegistry', () => { + describe('setup', () => { + test('throws when registering duplicate id', () => { + const setup = new FeatureCatalogueRegistry().setup(); + setup.register(DASHBOARD_FEATURE); + expect(() => setup.register(DASHBOARD_FEATURE)).toThrowErrorMatchingInlineSnapshot( + `"Feature with id [dashboard] has already been registered. Use a unique id."` + ); + }); + }); + + describe('start', () => { + describe('capabilities filtering', () => { + test('retains items with no entry in capabilities', () => { + const service = new FeatureCatalogueRegistry(); + service.setup().register(DASHBOARD_FEATURE); + const capabilities = { catalogue: {} } as any; + expect(service.start({ capabilities }).get()).toEqual([DASHBOARD_FEATURE]); + }); + + test('retains items with true in capabilities', () => { + const service = new FeatureCatalogueRegistry(); + service.setup().register(DASHBOARD_FEATURE); + const capabilities = { catalogue: { dashboard: true } } as any; + expect(service.start({ capabilities }).get()).toEqual([DASHBOARD_FEATURE]); + }); + + test('removes items with false in capabilities', () => { + const service = new FeatureCatalogueRegistry(); + service.setup().register(DASHBOARD_FEATURE); + const capabilities = { catalogue: { dashboard: false } } as any; + expect(service.start({ capabilities }).get()).toEqual([]); + }); + }); + }); + + describe('title sorting', () => { + test('sorts by title ascending', () => { + const service = new FeatureCatalogueRegistry(); + const setup = service.setup(); + setup.register({ id: '1', title: 'Orange' } as any); + setup.register({ id: '2', title: 'Apple' } as any); + setup.register({ id: '3', title: 'Banana' } as any); + const capabilities = { catalogue: {} } as any; + expect(service.start({ capabilities }).get()).toEqual([ + { id: '2', title: 'Apple' }, + { id: '3', title: 'Banana' }, + { id: '1', title: 'Orange' }, + ]); + }); + }); +}); diff --git a/src/plugins/feature_catalogue/public/services/feature_catalogue_registry.ts b/src/plugins/feature_catalogue/public/services/feature_catalogue_registry.ts new file mode 100644 index 0000000000000..6ab342f37dfd9 --- /dev/null +++ b/src/plugins/feature_catalogue/public/services/feature_catalogue_registry.ts @@ -0,0 +1,86 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Capabilities } from 'src/core/public'; +import { IconType } from '@elastic/eui'; + +/** @public */ +export enum FeatureCatalogueCategory { + ADMIN = 'admin', + DATA = 'data', + OTHER = 'other', +} + +/** @public */ +export interface FeatureCatalogueEntry { + /** Unique string identifier for this feature. */ + readonly id: string; + /** Title of feature displayed to the user. */ + readonly title: string; + /** {@link FeatureCatalogueCategory} to display this feature in. */ + readonly category: FeatureCatalogueCategory; + /** One-line description of feature displayed to the user. */ + readonly description: string; + /** EUI `IconType` for icon to be displayed to the user. EUI supports any known EUI icon, SVG URL, or ReactElement. */ + readonly icon: IconType; + /** URL path to link to this future. Should not include the basePath. */ + readonly path: string; + /** Whether or not this link should be shown on the front page of Kibana. */ + readonly showOnHomePage: boolean; +} + +export class FeatureCatalogueRegistry { + private readonly features = new Map(); + + public setup() { + return { + register: (feature: FeatureCatalogueEntry) => { + if (this.features.has(feature.id)) { + throw new Error( + `Feature with id [${feature.id}] has already been registered. Use a unique id.` + ); + } + + this.features.set(feature.id, feature); + }, + }; + } + + public start({ capabilities }: { capabilities: Capabilities }) { + return { + get: (): readonly FeatureCatalogueEntry[] => + [...this.features.values()] + .filter(entry => capabilities.catalogue[entry.id] !== false) + .sort(compareByKey('title')), + }; + } +} + +export type FeatureCatalogueRegistrySetup = ReturnType; +export type FeatureCatalogueRegistryStart = ReturnType; + +const compareByKey = (key: keyof T) => (left: T, right: T) => { + if (left[key] < right[key]) { + return -1; + } else if (left[key] > right[key]) { + return 1; + } else { + return 0; + } +}; diff --git a/src/plugins/feature_catalogue/public/services/index.ts b/src/plugins/feature_catalogue/public/services/index.ts new file mode 100644 index 0000000000000..17433264f5a42 --- /dev/null +++ b/src/plugins/feature_catalogue/public/services/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export * from './feature_catalogue_registry';