Skip to content

Commit

Permalink
[WIP] Add help menu item to header (#29664)
Browse files Browse the repository at this point in the history
* Added static help menu item to header

* Update default message

* [chrome] add help menu extension point apis

* [chrome/headerExtension] fix test file name

* prettier

* Insert doclink and version dynamically

* Add missing i18n

* Lowercase `v` for version

* Smaller width for popover
  • Loading branch information
cchaos authored Feb 1, 2019
1 parent 11946fd commit c0f50a1
Show file tree
Hide file tree
Showing 21 changed files with 406 additions and 45 deletions.
29 changes: 27 additions & 2 deletions src/core/public/chrome/chrome_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,29 @@ Array [
],
Array [],
]
`);
});
});

describe('help extension', () => {
it('updates/emits the current help extension', async () => {
const service = new ChromeService();
const start = service.start();
const promise = start
.getHelpExtension$()
.pipe(toArray())
.toPromise();

start.setHelpExtension(() => () => undefined);
start.setHelpExtension(undefined);
service.stop();

await expect(promise).resolves.toMatchInlineSnapshot(`
Array [
undefined,
[Function],
undefined,
]
`);
});
});
Expand All @@ -258,7 +281,8 @@ describe('stop', () => {
start.getApplicationClasses$(),
start.getIsCollapsed$(),
start.getBreadcrumbs$(),
start.getIsVisible$()
start.getIsVisible$(),
start.getHelpExtension$()
).toPromise();

service.stop();
Expand All @@ -276,7 +300,8 @@ describe('stop', () => {
start.getApplicationClasses$(),
start.getIsCollapsed$(),
start.getBreadcrumbs$(),
start.getIsVisible$()
start.getIsVisible$(),
start.getHelpExtension$()
).toPromise()
).resolves.toBe(undefined);
});
Expand Down
15 changes: 15 additions & 0 deletions src/core/public/chrome/chrome_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export interface Breadcrumb {
'data-test-subj'?: string;
}

export type HelpExtension = (element: HTMLDivElement) => (() => void);

export class ChromeService {
private readonly stop$ = new Rx.ReplaySubject(1);

Expand All @@ -50,6 +52,7 @@ export class ChromeService {
const isVisible$ = new Rx.BehaviorSubject(true);
const isCollapsed$ = new Rx.BehaviorSubject(!!localStorage.getItem(IS_COLLAPSED_KEY));
const applicationClasses$ = new Rx.BehaviorSubject<Set<string>>(new Set());
const helpExtension$ = new Rx.BehaviorSubject<HelpExtension | undefined>(undefined);
const breadcrumbs$ = new Rx.BehaviorSubject<Breadcrumb[]>([]);

return {
Expand Down Expand Up @@ -154,6 +157,18 @@ export class ChromeService {
setBreadcrumbs: (newBreadcrumbs: Breadcrumb[]) => {
breadcrumbs$.next(newBreadcrumbs);
},

/**
* Get an observable of the current custom help conttent
*/
getHelpExtension$: () => helpExtension$.pipe(takeUntil(this.stop$)),

/**
* Override the current set of breadcrumbs
*/
setHelpExtension: (helpExtension?: HelpExtension) => {
helpExtension$.next(helpExtension);
},
};
}

Expand Down
8 changes: 7 additions & 1 deletion src/core/public/chrome/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,10 @@
* under the License.
*/

export { Breadcrumb, ChromeService, ChromeStartContract, Brand } from './chrome_service';
export {
Breadcrumb,
ChromeService,
ChromeStartContract,
Brand,
HelpExtension,
} from './chrome_service';
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Array [
"ui/chrome/api/ui_settings",
"ui/chrome/api/injected_vars",
"ui/chrome/api/controls",
"ui/chrome/api/help_extension",
"ui/chrome/api/theme",
"ui/chrome/api/breadcrumbs",
"ui/chrome/services/global_nav_state",
Expand All @@ -28,6 +29,7 @@ Array [
"ui/chrome/api/ui_settings",
"ui/chrome/api/injected_vars",
"ui/chrome/api/controls",
"ui/chrome/api/help_extension",
"ui/chrome/api/theme",
"ui/chrome/api/breadcrumbs",
"ui/chrome/services/global_nav_state",
Expand Down
19 changes: 19 additions & 0 deletions src/core/public/legacy_platform/legacy_platform_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@ jest.mock('ui/chrome/api/controls', () => {
};
});

const mockChromeHelpExtensionInit = jest.fn();
jest.mock('ui/chrome/api/help_extension', () => {
mockLoadOrder.push('ui/chrome/api/help_extension');
return {
__newPlatformInit__: mockChromeHelpExtensionInit,
};
});

const mockChromeThemeInit = jest.fn();
jest.mock('ui/chrome/api/theme', () => {
mockLoadOrder.push('ui/chrome/api/theme');
Expand Down Expand Up @@ -269,6 +277,17 @@ describe('#start()', () => {
expect(mockChromeControlsInit).toHaveBeenCalledWith(chromeStartContract);
});

it('passes chrome service to ui/chrome/api/help_extension', () => {
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
});

legacyPlatform.start(defaultStartDeps);

expect(mockChromeHelpExtensionInit).toHaveBeenCalledTimes(1);
expect(mockChromeHelpExtensionInit).toHaveBeenCalledWith(chromeStartContract);
});

it('passes chrome service to ui/chrome/api/theme', () => {
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
Expand Down
1 change: 1 addition & 0 deletions src/core/public/legacy_platform/legacy_platform_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export class LegacyPlatformService {
require('ui/chrome/api/ui_settings').__newPlatformInit__(uiSettings);
require('ui/chrome/api/injected_vars').__newPlatformInit__(injectedMetadata);
require('ui/chrome/api/controls').__newPlatformInit__(chrome);
require('ui/chrome/api/help_extension').__newPlatformInit__(chrome);
require('ui/chrome/api/theme').__newPlatformInit__(chrome);
require('ui/chrome/api/breadcrumbs').__newPlatformInit__(chrome);
require('ui/chrome/services/global_nav_state').__newPlatformInit__(chrome);
Expand Down
1 change: 1 addition & 0 deletions src/ui/public/chrome/api/angular.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export function initAngularApi(chrome, internals) {
})
.run(internals.capture$httpLoadingCount)
.run(internals.$setupBreadcrumbsAutoClear)
.run(internals.$setupHelpExtensionAutoClear)
.run(internals.$initNavLinksDeepWatch)
.run(($location, $rootScope, Private, config) => {
chrome.getFirstPathSegment = () => {
Expand Down
97 changes: 97 additions & 0 deletions src/ui/public/chrome/api/help_extension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* 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 { IRootScopeService } from 'angular';

import { ChromeStartContract, HelpExtension } from '../../../../core/public/chrome';

let newPlatformChrome: ChromeStartContract;
export function __newPlatformInit__(instance: ChromeStartContract) {
if (newPlatformChrome) {
throw new Error('ui/chrome/api/help_extension is already initialized');
}

newPlatformChrome = instance;
}

export type HelpExtensionApi = ReturnType<typeof createHelpExtensionApi>['helpExtension'];
export { HelpExtension };

function createHelpExtensionApi() {
/**
* reset helpExtensionSetSinceRouteChange any time the helpExtension changes, even
* if it was done directly through the new platform
*/
let helpExtensionSetSinceRouteChange = false;
newPlatformChrome.getHelpExtension$().subscribe({
next() {
helpExtensionSetSinceRouteChange = true;
},
});

return {
helpExtension: {
/**
* Set the custom help extension, or clear it by passing undefined. This
* will be rendered within the help popover in the header
*/
set: (helpExtension: HelpExtension | undefined) => {
newPlatformChrome.setHelpExtension(helpExtension);
},

/**
* Get the current help extension that should be rendered in the header
*/
get$: () => newPlatformChrome.getHelpExtension$(),
},

/**
* internal angular run function that will be called when angular bootstraps and
* lets us integrate with the angular router so that we can automatically clear
* the helpExtension if we switch to a Kibana app that does not set its own
* helpExtension
*/
$setupHelpExtensionAutoClear: ($rootScope: IRootScopeService, $injector: any) => {
const $route = $injector.has('$route') ? $injector.get('$route') : {};

$rootScope.$on('$routeChangeStart', () => {
helpExtensionSetSinceRouteChange = false;
});

$rootScope.$on('$routeChangeSuccess', () => {
const current = $route.current || {};

if (helpExtensionSetSinceRouteChange || (current.$$route && current.$$route.redirectTo)) {
return;
}

newPlatformChrome.setHelpExtension(current.helpExtension);
});
},
};
}

export function initHelpExtensionApi(
chrome: { [key: string]: any },
internal: { [key: string]: any }
) {
const { helpExtension, $setupHelpExtensionAutoClear } = createHelpExtensionApi();
chrome.helpExtension = helpExtension;
internal.$setupHelpExtensionAutoClear = $setupHelpExtensionAutoClear;
}
2 changes: 2 additions & 0 deletions src/ui/public/chrome/chrome.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { initLoadingCountApi } from './api/loading_count';
import { initSavedObjectClient } from './api/saved_object_client';
import { initChromeBasePathApi } from './api/base_path';
import { initChromeInjectedVarsApi } from './api/injected_vars';
import { initHelpExtensionApi } from './api/help_extension';

export const chrome = {};
const internals = _.defaults(
Expand All @@ -70,6 +71,7 @@ initChromeInjectedVarsApi(chrome);
initChromeNavApi(chrome, internals);
initBreadcrumbsApi(chrome, internals);
initLoadingCountApi(chrome, internals);
initHelpExtensionApi(chrome, internals);
initAngularApi(chrome, internals);
initChromeControlsApi(chrome);
templateApi(chrome, internals);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,7 @@
left: 0;
}
}

.chrHeaderHelpMenu__version {
text-transform: none;
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,12 @@ import {
} from '@elastic/eui';

import { HeaderBreadcrumbs } from './header_breadcrumbs';
import { HeaderHelpMenu } from './header_help_menu';
import { HeaderNavControls } from './header_nav_controls';

import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import chrome, { NavLink } from 'ui/chrome';
import { HelpExtension } from 'ui/chrome';
import { RecentlyAccessedHistoryItem } from 'ui/persisted_log';
import { ChromeHeaderNavControlsRegistry } from 'ui/registry/chrome_header_nav_controls';
import { relativeToAbsolute } from 'ui/url/relative_to_absolute';
Expand All @@ -69,6 +71,7 @@ interface Props {
isVisible: boolean;
navLinks$: Rx.Observable<NavLink[]>;
recentlyAccessed$: Rx.Observable<RecentlyAccessedHistoryItem[]>;
helpExtension$: Rx.Observable<HelpExtension>;
navControls: ChromeHeaderNavControlsRegistry;
intl: InjectedIntl;
}
Expand Down Expand Up @@ -169,7 +172,7 @@ class HeaderUI extends Component<Props, State> {
}

public render() {
const { appTitle, breadcrumbs$, isVisible, navControls } = this.props;
const { appTitle, breadcrumbs$, isVisible, navControls, helpExtension$ } = this.props;
const { navLinks, recentlyAccessed } = this.state;

if (!isVisible) {
Expand All @@ -195,9 +198,14 @@ class HeaderUI extends Component<Props, State> {
<HeaderBreadcrumbs appTitle={appTitle} breadcrumbs$={breadcrumbs$} />

<EuiHeaderSection side="right">
<EuiHeaderSectionItem>
<HeaderHelpMenu helpExtension$={helpExtension$} />
</EuiHeaderSectionItem>

<HeaderNavControls navControls={rightNavControls} />
</EuiHeaderSection>
</EuiHeader>

<EuiOutsideClickDetector
onOutsideClick={() => this.collapseDrawer()}
isDisabled={this.state.outsideClickDisabled}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,12 @@

import { mount } from 'enzyme';
import React from 'react';
import { NavControl, NavControlSide } from '../';
import { HeaderNavControl } from './header_nav_control';

describe('HeaderNavControl', () => {
const defaultNavControl = { name: '', order: 1, side: NavControlSide.Right };
import { HeaderExtension } from './header_extension';

describe('HeaderExtension', () => {
it('calls navControl.render with div node', () => {
const renderSpy = jest.fn();
const navControl = { ...defaultNavControl, render: renderSpy } as NavControl;

mount(<HeaderNavControl navControl={navControl} />);
mount(<HeaderExtension extension={renderSpy} />);

expect(renderSpy.mock.calls.length).toEqual(1);

Expand All @@ -40,9 +35,8 @@ describe('HeaderNavControl', () => {
it('calls unrender callback when unmounted', () => {
const unrenderSpy = jest.fn();
const render = () => unrenderSpy;
const navControl = { ...defaultNavControl, render } as NavControl;

const wrapper = mount(<HeaderNavControl navControl={navControl} />);
const wrapper = mount(<HeaderExtension extension={render} />);

wrapper.unmount();
expect(unrenderSpy.mock.calls.length).toEqual(1);
Expand Down
Loading

0 comments on commit c0f50a1

Please sign in to comment.