Allow plugins to register top nav menu items (regression fix) (#48542)

* Move top nav menu to its own plugin
Allow registering additional options from other plugins
Added demo plugin

* Added functional test to validate top nav registration

* Improved names

* Rename array

* Fixed lens tests

* Deleted old NavBarExtensionsRegistryProvider

* Fixed top nav menu test

* Attempt fixing test by clearing ui_actions on stop

* temporary disable test
This commit is contained in:
Liza Katz 2019-10-28 10:40:21 +02:00 committed by GitHub
parent 795d1caa5a
commit 6bb30e7190
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 487 additions and 44 deletions

1
.github/CODEOWNERS vendored
View file

@ -10,6 +10,7 @@
/src/plugins/data/ @elastic/kibana-app-arch
/src/plugins/kibana_utils/ @elastic/kibana-app-arch
/src/plugins/kibana_react/ @elastic/kibana-app-arch
/src/plugins/navigation/ @elastic/kibana-app-arch
# APM
/x-pack/legacy/plugins/apm/ @elastic/apm-ui

1
.github/labeler.yml vendored
View file

@ -2,6 +2,7 @@
- src/plugins/data/**/*
- src/plugins/embeddable/**/*
- src/plugins/kibana_react/**/*
- src/plugins/navigation/**/*
- src/plugins/kibana_utils/**/*
- src/legacy/core_plugins/dashboard_embeddable_container/**/*
- src/legacy/core_plugins/data/**/*

View file

@ -4,6 +4,7 @@
"data": ["src/legacy/core_plugins/data", "src/plugins/data"],
"expressions": "src/legacy/core_plugins/expressions",
"kibana_react": "src/legacy/core_plugins/kibana_react",
"navigation": "src/legacy/core_plugins/navigation",
"server": "src/legacy/server",
"console": "src/legacy/core_plugins/console",
"core": "src/core",

View file

@ -1132,7 +1132,7 @@ import { setup, start } from '../core_plugins/visualizations/public/legacy';
| `import 'ui/filter_bar'` | `import { FilterBar } from '../data/public'` | `import '../data/public/legacy` should be called to load legacy directives |
| `import 'ui/query_bar'` | `import { QueryBar, QueryBarInput } from '../data/public'` | Directives are deprecated. |
| `import 'ui/search_bar'` | `import { SearchBar } from '../data/public'` | Directive is deprecated. |
| `import 'ui/kbn_top_nav'` | `import { TopNavMenu } from '../kibana_react/public'` | Directive is still available in `ui/kbn_top_nav`. |
| `import 'ui/kbn_top_nav'` | `import { TopNavMenu } from '../navigation/public'` | Directive is still available in `ui/kbn_top_nav`. |
| `ui/saved_objects/components/saved_object_finder` | `import { SavedObjectFinder } from '../kibana_react/public'` | |
| `core_plugins/interpreter` | `data.expressions` | still in progress |
| `ui/courier` | `data.search` | still in progress |

View file

@ -1,5 +1,3 @@
@import 'src/legacy/ui/public/styles/styling_constants';
@import './top_nav_menu/index';
@import './markdown/index';

View file

@ -23,6 +23,4 @@
// of the ExpressionExectorService
/** @public types */
export { TopNavMenu, TopNavMenuData } from './top_nav_menu';
export { Markdown, MarkdownSimple } from './markdown';

View file

@ -0,0 +1,42 @@
/*
* 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 { resolve } from 'path';
import { Legacy } from '../../../../kibana';
// eslint-disable-next-line import/no-default-export
export default function NavigationPlugin(kibana: any) {
const config: Legacy.PluginSpecOptions = {
id: 'navigation',
require: [],
publicDir: resolve(__dirname, 'public'),
config: (Joi: any) => {
return Joi.object({
enabled: Joi.boolean().default(true),
}).default();
},
init: (server: Legacy.Server) => ({}),
uiExports: {
injectDefaultVars: () => ({}),
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
},
};
return new kibana.Plugin(config);
}

View file

@ -0,0 +1,4 @@
{
"name": "navigation",
"version": "kibana"
}

View file

@ -0,0 +1,3 @@
@import 'src/legacy/ui/public/styles/styling_constants';
@import './top_nav_menu/index';

View file

@ -0,0 +1,32 @@
/*
* 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.
*/
// TODO these are imports from the old plugin world.
// Once the new platform is ready, they can get removed
// and handled by the platform itself in the setup method
// of the ExpressionExectorService
/** @public types */
export { TopNavMenu, TopNavMenuData } from './top_nav_menu';
export { NavigationSetup, NavigationStart } from './plugin';
import { NavigationPlugin as Plugin } from './plugin';
export function plugin() {
return new Plugin();
}

View file

@ -0,0 +1,30 @@
/*
* 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 { npSetup, npStart } from 'ui/new_platform';
import { start as dataShim } from '../../data/public/legacy';
import { plugin } from '.';
const navPlugin = plugin();
export const setup = navPlugin.setup(npSetup.core);
export const start = navPlugin.start(npStart.core, {
data: dataShim,
});

View file

@ -0,0 +1,74 @@
/*
* 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 { CoreSetup, CoreStart, Plugin } from 'kibana/public';
import { TopNavMenuExtensionsRegistry, TopNavMenuExtensionsRegistrySetup } from './top_nav_menu';
import { createTopNav } from './top_nav_menu/create_top_nav_menu';
import { TopNavMenuProps } from './top_nav_menu/top_nav_menu';
import { DataStart } from '../../data/public';
/**
* Interface for this plugin's returned `setup` contract.
*
* @public
*/
export interface NavigationSetup {
registerMenuItem: TopNavMenuExtensionsRegistrySetup['register'];
}
/**
* Interface for this plugin's returned `start` contract.
*
* @public
*/
export interface NavigationStart {
ui: {
TopNavMenu: React.ComponentType<TopNavMenuProps>;
};
}
export interface NavigationPluginStartDependencies {
data: DataStart;
}
export class NavigationPlugin implements Plugin<NavigationSetup, NavigationStart> {
private readonly topNavMenuExtensionsRegistry: TopNavMenuExtensionsRegistry = new TopNavMenuExtensionsRegistry();
public setup(core: CoreSetup): NavigationSetup {
return {
registerMenuItem: this.topNavMenuExtensionsRegistry.register.bind(
this.topNavMenuExtensionsRegistry
),
};
}
public start(core: CoreStart, { data }: NavigationPluginStartDependencies): NavigationStart {
const extensions = this.topNavMenuExtensionsRegistry.getAll();
return {
ui: {
TopNavMenu: createTopNav(data, extensions),
},
};
}
public stop() {
this.topNavMenuExtensionsRegistry.clear();
}
}

View file

@ -0,0 +1,31 @@
/*
* 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 React from 'react';
import { TopNavMenuProps, TopNavMenu } from './top_nav_menu';
import { TopNavMenuData } from './top_nav_menu_data';
import { DataStart } from '../../../../core_plugins/data/public';
export function createTopNav(data: DataStart, extraConfig: TopNavMenuData[]) {
return (props: TopNavMenuProps) => {
const config = (props.config || []).concat(extraConfig);
return <TopNavMenu {...props} data={data} config={config} />;
};
}

View file

@ -19,3 +19,4 @@
export { TopNavMenu } from './top_nav_menu';
export { TopNavMenuData } from './top_nav_menu_data';
export * from './top_nav_menu_extensions_registry';

View file

@ -27,21 +27,11 @@ const timefilterSetupMock = timefilterServiceMock.createSetupContract();
jest.mock('ui/new_platform');
jest.mock('../../../../../../src/legacy/core_plugins/data/public/legacy', () => ({
start: {
ui: {
SearchBar: () => {},
},
},
setup: {},
}));
jest.mock('../../../../core_plugins/data/public', () => {
return {
const dataShim = {
ui: {
SearchBar: () => <div className="searchBar"></div>,
SearchBarProps: {},
};
});
},
};
describe('TopNavMenu', () => {
const TOP_NAV_ITEM_SELECTOR = 'TopNavMenuItem';
@ -84,7 +74,12 @@ describe('TopNavMenu', () => {
it('Should render search bar', () => {
const component = shallowWithIntl(
<TopNavMenu appName={'test'} showSearchBar={true} timeHistory={timefilterSetupMock.history} />
<TopNavMenu
appName={'test'}
showSearchBar={true}
timeHistory={timefilterSetupMock.history}
data={dataShim as any}
/>
);
expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(0);

View file

@ -24,13 +24,13 @@ import { I18nProvider } from '@kbn/i18n/react';
import { TopNavMenuData } from './top_nav_menu_data';
import { TopNavMenuItem } from './top_nav_menu_item';
import { SearchBarProps } from '../../../../core_plugins/data/public';
import { start as data } from '../../../data/public/legacy';
import { SearchBarProps, DataStart } from '../../../../core_plugins/data/public';
type Props = Partial<SearchBarProps> & {
export type TopNavMenuProps = Partial<SearchBarProps> & {
appName: string;
config?: TopNavMenuData[];
showSearchBar?: boolean;
data?: DataStart;
};
/*
@ -42,8 +42,7 @@ type Props = Partial<SearchBarProps> & {
*
**/
export function TopNavMenu(props: Props) {
const { SearchBar } = data.ui;
export function TopNavMenu(props: TopNavMenuProps) {
const { config, showSearchBar, ...searchBarProps } = props;
function renderItems() {
if (!config) return;
@ -58,7 +57,8 @@ export function TopNavMenu(props: Props) {
function renderSearchBar() {
// Validate presense of all required fields
if (!showSearchBar) return;
if (!showSearchBar || !props.data) return;
const { SearchBar } = props.data.ui;
return <SearchBar {...searchBarProps} />;
}

View file

@ -0,0 +1,46 @@
/*
* 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 { TopNavMenuData } from './top_nav_menu_data';
export class TopNavMenuExtensionsRegistry {
private menuItems: TopNavMenuData[];
constructor() {
this.menuItems = [];
}
/** @public **/
// Items registered into this registry will be appended to any TopNavMenu rendered in any application.
public register(menuItem: TopNavMenuData) {
this.menuItems.push(menuItem);
}
/** @internal **/
public getAll() {
return this.menuItems;
}
/** @internal **/
public clear() {
this.menuItems.length = 0;
}
}
export type TopNavMenuExtensionsRegistrySetup = Pick<TopNavMenuExtensionsRegistry, 'register'>;

View file

@ -20,7 +20,7 @@
import 'ngreact';
import { wrapInI18nContext } from 'ui/i18n';
import { uiModules } from 'ui/modules';
import { TopNavMenu } from '../../../core_plugins/kibana_react/public';
import { start as navigation } from '../../../core_plugins/navigation/public/legacy';
const module = uiModules.get('kibana');
@ -76,6 +76,7 @@ export function createTopNavDirective() {
module.directive('kbnTopNav', createTopNavDirective);
export function createTopNavHelper(reactDirective) {
const { TopNavMenu } = navigation.ui;
return reactDirective(
wrapInI18nContext(TopNavMenu),
[

View file

@ -52,5 +52,8 @@ export class UiActionsPlugin implements Plugin<IUiActionsSetup, IUiActionsStart>
return this.api;
}
public stop() {}
public stop() {
this.actions.clear();
this.triggers.clear();
}
}

View file

@ -17,12 +17,14 @@
* under the License.
*/
import { uiRegistry } from './_registry';
export const NavBarExtensionsRegistryProvider = uiRegistry({
name: 'navbarExtensions',
index: ['name'],
group: ['appName'],
order: ['order']
});
export default function (kibana) {
return new kibana.Plugin({
uiExports: {
app: {
title: 'Top Nav Menu test',
description: 'This is a sample plugin for the functional tests.',
main: 'plugins/kbn_tp_top_nav/app',
}
}
});
}

View file

@ -0,0 +1,9 @@
{
"name": "kbn_tp_top_nav",
"version": "1.0.0",
"kibana": {
"version": "kibana",
"templateVersion": "1.0.0"
},
"license": "Apache-2.0"
}

View file

@ -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 React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { uiModules } from 'ui/modules';
import chrome from 'ui/chrome';
// This is required so some default styles and required scripts/Angular modules are loaded,
// or the timezone setting is correctly applied.
import 'ui/autoload/all';
import { AppWithTopNav } from './top_nav';
const app = uiModules.get('apps/topnavDemoPlugin', ['kibana']);
app.config($locationProvider => {
$locationProvider.html5Mode({
enabled: false,
requireBase: false,
rewriteLinks: false,
});
});
function RootController($scope, $element) {
const domNode = $element[0];
// render react to DOM
render(<AppWithTopNav />, domNode);
// unmount react on controller destroy
$scope.$on('$destroy', () => {
unmountComponentAtNode(domNode);
});
}
chrome.setRootController('topnavDemoPlugin', RootController);

View file

@ -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 React, { Component } from 'react';
import {
setup as navSetup,
start as navStart,
} from '../../../../../src/legacy/core_plugins/navigation/public/legacy';
const customExtension = {
id: 'registered-prop',
label: 'Registered Button',
description: 'Registered Demo',
run() {},
testId: 'demoRegisteredNewButton',
};
navSetup.registerMenuItem(customExtension);
export class AppWithTopNav extends Component {
public render() {
const { TopNavMenu } = navStart.ui;
const config = [
{
id: 'new',
label: 'New Button',
description: 'New Demo',
run() {},
testId: 'demoNewButton',
},
];
return (
<TopNavMenu appName="demo-app" config={config}>
Hey
</TopNavMenu>
);
}
}

View file

@ -0,0 +1,15 @@
{
"extends": "../../../../tsconfig.json",
"compilerOptions": {
"outDir": "./target",
"skipLibCheck": true
},
"include": [
"index.ts",
"public/**/*.ts",
"public/**/*.tsx",
"server/**/*.ts",
"../../../../typings/**/*",
],
"exclude": []
}

View file

@ -23,5 +23,6 @@ export default function ({ loadTestFile }) {
loadTestFile(require.resolve('./legacy_plugins'));
loadTestFile(require.resolve('./server_plugins'));
loadTestFile(require.resolve('./ui_plugins'));
loadTestFile(require.resolve('./top_nav'));
});
}

View file

@ -0,0 +1,40 @@
/*
* 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 expect from '@kbn/expect';
export default function ({ getService, getPageObjects }) {
const PageObjects = getPageObjects(['common']);
const browser = getService('browser');
const testSubjects = getService('testSubjects');
describe.skip('top nav', function describeIndexTests() {
before(async () => {
const url = `${PageObjects.common.getHostPort()}/app/kbn_tp_top_nav/`;
await browser.get(url);
});
it('Shows registered menu items', async () => {
const ownMenuItem = await testSubjects.find('demoNewButton');
expect(await ownMenuItem.getVisibleText()).to.be('New Button');
const demoRegisteredNewButton = await testSubjects.find('demoRegisteredNewButton');
expect(await demoRegisteredNewButton.getVisibleText()).to.be('Registered Button');
});
});
}

View file

@ -17,17 +17,22 @@ import { mount } from 'enzyme';
import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks';
const dataStartMock = dataPluginMock.createStartContract();
import {
TopNavMenu,
TopNavMenuData,
} from '../../../../../../src/legacy/core_plugins/kibana_react/public';
import { TopNavMenuData } from '../../../../../../src/legacy/core_plugins/navigation/public';
import { DataStart } from '../../../../../../src/legacy/core_plugins/data/public';
import { coreMock } from 'src/core/public/mocks';
jest.mock('../../../../../../src/legacy/core_plugins/kibana_react/public', () => ({
TopNavMenu: jest.fn(() => null),
jest.mock('../../../../../../src/legacy/core_plugins/navigation/public/legacy', () => ({
start: {
ui: {
TopNavMenu: jest.fn(() => null),
},
},
}));
import { start as navigation } from '../../../../../../src/legacy/core_plugins/navigation/public/legacy';
const { TopNavMenu } = navigation.ui;
jest.mock('ui/new_platform');
jest.mock('../persistence');
jest.mock('src/core/public');

View file

@ -20,7 +20,7 @@ import {
Query,
} from 'src/legacy/core_plugins/data/public';
import { Filter } from '@kbn/es-query';
import { TopNavMenu } from '../../../../../../src/legacy/core_plugins/kibana_react/public';
import { start as navigation } from '../../../../../../src/legacy/core_plugins/navigation/public/legacy';
import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public';
import { Document, SavedObjectStore } from '../persistence';
import { EditorFrameInstance } from '../types';
@ -163,6 +163,8 @@ export function App({
[]
);
const { TopNavMenu } = navigation.ui;
return (
<I18nProvider>
<KibanaContextProvider