Allow registered applications to hide Kibana chrome (#49795) (#50524)

* Allow registered applications to hide Kibana chrome

* Fix bug in flipped value of application chromeHidden

* Add additional test for app chrome hidden versus chrome visibility

* Rename chromeHidden to chromeless

* Default chrome service app hidden observable to same value as force hidden

* Consolidate force hiding in chrome, add functional tests

* Move chromeless flag to App interface to prevent legacy applications from specifying

* Address review nits to improve separation
This commit is contained in:
Eli Perelman 2019-11-13 16:30:59 -06:00 committed by GitHub
parent 53af425c62
commit 7bf7f1e50a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 550 additions and 234 deletions

View file

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [App](./kibana-plugin-public.app.md) &gt; [chromeless](./kibana-plugin-public.app.chromeless.md)
## App.chromeless property
Hide the UI chrome when the application is mounted. Defaults to `false`<!-- -->. Takes precedence over chrome service visibility settings.
<b>Signature:</b>
```typescript
chromeless?: boolean;
```

View file

@ -16,5 +16,6 @@ export interface App extends AppBase
| Property | Type | Description |
| --- | --- | --- |
| [chromeless](./kibana-plugin-public.app.chromeless.md) | <code>boolean</code> | Hide the UI chrome when the application is mounted. Defaults to <code>false</code>. Takes precedence over chrome service visibility settings. |
| [mount](./kibana-plugin-public.app.mount.md) | <code>(context: AppMountContext, params: AppMountParameters) =&gt; AppUnmount &#124; Promise&lt;AppUnmount&gt;</code> | A mount function called when the user navigates to this app's route. |

View file

@ -21,12 +21,13 @@ How to configure react-router with a base path:
export class MyPlugin implements Plugin {
setup({ application }) {
application.register({
id: 'my-app',
async mount(context, params) {
const { renderApp } = await import('./application');
return renderApp(context, params);
},
});
id: 'my-app',
async mount(context, params) {
const { renderApp } = await import('./application');
return renderApp(context, params);
},
});
}
}
```

View file

@ -80,6 +80,12 @@ export interface App extends AppBase {
* @returns An unmounting function that will be called to unmount the application.
*/
mount: (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise<AppUnmount>;
/**
* Hide the UI chrome when the application is mounted. Defaults to `false`.
* Takes precedence over chrome service visibility settings.
*/
chromeless?: boolean;
}
/** @internal */
@ -145,12 +151,13 @@ export interface AppMountParameters {
* export class MyPlugin implements Plugin {
* setup({ application }) {
* application.register({
* id: 'my-app',
* async mount(context, params) {
* const { renderApp } = await import('./application');
* return renderApp(context, params);
* },
* });
* id: 'my-app',
* async mount(context, params) {
* const { renderApp } = await import('./application');
* return renderApp(context, params);
* },
* });
* }
* }
* ```
*

View file

@ -26,351 +26,423 @@ import { applicationServiceMock } from '../application/application_service.mock'
import { httpServiceMock } from '../http/http_service.mock';
import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock';
import { notificationServiceMock } from '../notifications/notifications_service.mock';
import { ChromeService } from './chrome_service';
import { docLinksServiceMock } from '../doc_links/doc_links_service.mock';
import { ChromeService } from './chrome_service';
import { App } from '../application';
class FakeApp implements App {
public title = `${this.id} App`;
public mount = () => () => {};
constructor(public id: string, public chromeless?: boolean) {}
}
const store = new Map();
const originalLocalStorage = window.localStorage;
(window as any).localStorage = {
setItem: (key: string, value: string) => store.set(String(key), String(value)),
getItem: (key: string) => store.get(String(key)),
removeItem: (key: string) => store.delete(String(key)),
};
function defaultStartDeps() {
return {
function defaultStartDeps(availableApps?: App[]) {
const deps = {
application: applicationServiceMock.createInternalStartContract(),
docLinks: docLinksServiceMock.createStartContract(),
http: httpServiceMock.createStartContract(),
injectedMetadata: injectedMetadataServiceMock.createStartContract(),
notifications: notificationServiceMock.createStartContract(),
};
if (availableApps) {
deps.application.availableApps = new Map(availableApps.map(app => [app.id, app]));
}
return deps;
}
async function start({
options = { browserSupportsCsp: true },
cspConfigMock = { warnLegacyBrowsers: true },
startDeps = defaultStartDeps(),
}: { options?: any; cspConfigMock?: any; startDeps?: ReturnType<typeof defaultStartDeps> } = {}) {
const service = new ChromeService(options);
if (cspConfigMock) {
startDeps.injectedMetadata.getCspConfig.mockReturnValue(cspConfigMock);
}
return {
service,
startDeps,
chrome: await service.start(startDeps),
};
}
beforeEach(() => {
store.clear();
window.history.pushState(undefined, '', '#/home?a=b');
});
afterAll(() => {
(window as any).localStorage = originalLocalStorage;
});
describe('start', () => {
it('adds legacy browser warning if browserSupportsCsp is disabled and warnLegacyBrowsers is enabled', async () => {
const service = new ChromeService({ browserSupportsCsp: false });
const startDeps = defaultStartDeps();
startDeps.injectedMetadata.getCspConfig.mockReturnValue({ warnLegacyBrowsers: true });
await service.start(startDeps);
const { startDeps } = await start({ options: { browserSupportsCsp: false } });
expect(startDeps.notifications.toasts.addWarning.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
"Your browser does not meet the security requirements for Kibana.",
],
]
`);
Array [
Array [
"Your browser does not meet the security requirements for Kibana.",
],
]
`);
});
it('does not add legacy browser warning if browser supports CSP', async () => {
const service = new ChromeService({ browserSupportsCsp: true });
const startDeps = defaultStartDeps();
startDeps.injectedMetadata.getCspConfig.mockReturnValue({ warnLegacyBrowsers: true });
await service.start(startDeps);
const { startDeps } = await start();
expect(startDeps.notifications.toasts.addWarning).not.toBeCalled();
});
it('does not add legacy browser warning if warnLegacyBrowsers is disabled', async () => {
const service = new ChromeService({ browserSupportsCsp: false });
const startDeps = defaultStartDeps();
startDeps.injectedMetadata.getCspConfig.mockReturnValue({ warnLegacyBrowsers: false });
await service.start(startDeps);
const { startDeps } = await start({
options: { browserSupportsCsp: false },
cspConfigMock: { warnLegacyBrowsers: false },
});
expect(startDeps.notifications.toasts.addWarning).not.toBeCalled();
});
describe('getComponent', () => {
it('returns a renderable React component', async () => {
const service = new ChromeService({ browserSupportsCsp: true });
const start = await service.start(defaultStartDeps());
const { chrome } = await start();
// Have to do some fanagling to get the type system and enzyme to accept this.
// Don't capture the snapshot because it's 600+ lines long.
expect(shallow(React.createElement(() => start.getHeaderComponent()))).toBeDefined();
expect(shallow(React.createElement(() => chrome.getHeaderComponent()))).toBeDefined();
});
});
describe('brand', () => {
it('updates/emits the brand as it changes', async () => {
const service = new ChromeService({ browserSupportsCsp: true });
const start = await service.start(defaultStartDeps());
const promise = start
const { chrome, service } = await start();
const promise = chrome
.getBrand$()
.pipe(toArray())
.toPromise();
start.setBrand({
chrome.setBrand({
logo: 'big logo',
smallLogo: 'not so big logo',
});
start.setBrand({
chrome.setBrand({
logo: 'big logo without small logo',
});
service.stop();
await expect(promise).resolves.toMatchInlineSnapshot(`
Array [
Object {},
Object {
"logo": "big logo",
"smallLogo": "not so big logo",
},
Object {
"logo": "big logo without small logo",
"smallLogo": undefined,
},
]
`);
Array [
Object {},
Object {
"logo": "big logo",
"smallLogo": "not so big logo",
},
Object {
"logo": "big logo without small logo",
"smallLogo": undefined,
},
]
`);
});
});
describe('visibility', () => {
it('updates/emits the visibility', async () => {
const service = new ChromeService({ browserSupportsCsp: true });
const start = await service.start(defaultStartDeps());
const promise = start
const { chrome, service } = await start();
const promise = chrome
.getIsVisible$()
.pipe(toArray())
.toPromise();
start.setIsVisible(true);
start.setIsVisible(false);
start.setIsVisible(true);
chrome.setIsVisible(true);
chrome.setIsVisible(false);
chrome.setIsVisible(true);
service.stop();
await expect(promise).resolves.toMatchInlineSnapshot(`
Array [
true,
true,
false,
true,
]
`);
Array [
true,
true,
false,
true,
]
`);
});
it('always emits false if embed query string is in hash when set up', async () => {
it('always emits false if embed query string is preset when set up', async () => {
window.history.pushState(undefined, '', '#/home?a=b&embed=true');
const service = new ChromeService({ browserSupportsCsp: true });
const start = await service.start(defaultStartDeps());
const promise = start
const { chrome, service } = await start();
const promise = chrome
.getIsVisible$()
.pipe(toArray())
.toPromise();
start.setIsVisible(true);
start.setIsVisible(false);
start.setIsVisible(true);
chrome.setIsVisible(true);
chrome.setIsVisible(false);
chrome.setIsVisible(true);
service.stop();
await expect(promise).resolves.toMatchInlineSnapshot(`
Array [
false,
false,
false,
false,
]
`);
Array [
false,
false,
false,
false,
]
`);
});
it('application-specified visibility on mount', async () => {
const startDeps = defaultStartDeps([
new FakeApp('alpha'), // An undefined `chromeless` is the same as setting to false.
new FakeApp('beta', true),
new FakeApp('gamma', false),
]);
const { availableApps, currentAppId$ } = startDeps.application;
const { chrome, service } = await start({ startDeps });
const promise = chrome
.getIsVisible$()
.pipe(toArray())
.toPromise();
[...availableApps.keys()].forEach(appId => currentAppId$.next(appId));
service.stop();
await expect(promise).resolves.toMatchInlineSnapshot(`
Array [
true,
true,
false,
true,
]
`);
});
it('changing visibility has no effect on chrome-hiding application', async () => {
const startDeps = defaultStartDeps([new FakeApp('alpha', true)]);
const { currentAppId$ } = startDeps.application;
const { chrome, service } = await start({ startDeps });
const promise = chrome
.getIsVisible$()
.pipe(toArray())
.toPromise();
currentAppId$.next('alpha');
chrome.setIsVisible(true);
service.stop();
await expect(promise).resolves.toMatchInlineSnapshot(`
Array [
true,
false,
false,
]
`);
});
});
describe('is collapsed', () => {
it('updates/emits isCollapsed', async () => {
const service = new ChromeService({ browserSupportsCsp: true });
const start = await service.start(defaultStartDeps());
const promise = start
const { chrome, service } = await start();
const promise = chrome
.getIsCollapsed$()
.pipe(toArray())
.toPromise();
start.setIsCollapsed(true);
start.setIsCollapsed(false);
start.setIsCollapsed(true);
chrome.setIsCollapsed(true);
chrome.setIsCollapsed(false);
chrome.setIsCollapsed(true);
service.stop();
await expect(promise).resolves.toMatchInlineSnapshot(`
Array [
false,
true,
false,
true,
]
`);
Array [
false,
true,
false,
true,
]
`);
});
it('only stores true in localStorage', async () => {
const service = new ChromeService({ browserSupportsCsp: true });
const start = await service.start(defaultStartDeps());
const { chrome } = await start();
start.setIsCollapsed(true);
chrome.setIsCollapsed(true);
expect(store.size).toBe(1);
start.setIsCollapsed(false);
chrome.setIsCollapsed(false);
expect(store.size).toBe(0);
});
});
describe('application classes', () => {
it('updates/emits the application classes', async () => {
const service = new ChromeService({ browserSupportsCsp: true });
const start = await service.start(defaultStartDeps());
const promise = start
const { chrome, service } = await start();
const promise = chrome
.getApplicationClasses$()
.pipe(toArray())
.toPromise();
start.addApplicationClass('foo');
start.addApplicationClass('foo');
start.addApplicationClass('bar');
start.addApplicationClass('bar');
start.addApplicationClass('baz');
start.removeApplicationClass('bar');
start.removeApplicationClass('foo');
chrome.addApplicationClass('foo');
chrome.addApplicationClass('foo');
chrome.addApplicationClass('bar');
chrome.addApplicationClass('bar');
chrome.addApplicationClass('baz');
chrome.removeApplicationClass('bar');
chrome.removeApplicationClass('foo');
service.stop();
await expect(promise).resolves.toMatchInlineSnapshot(`
Array [
Array [],
Array [
"foo",
],
Array [
"foo",
],
Array [
"foo",
"bar",
],
Array [
"foo",
"bar",
],
Array [
"foo",
"bar",
"baz",
],
Array [
"foo",
"baz",
],
Array [
"baz",
],
]
`);
Array [
Array [],
Array [
"foo",
],
Array [
"foo",
],
Array [
"foo",
"bar",
],
Array [
"foo",
"bar",
],
Array [
"foo",
"bar",
"baz",
],
Array [
"foo",
"baz",
],
Array [
"baz",
],
]
`);
});
});
describe('badge', () => {
it('updates/emits the current badge', async () => {
const service = new ChromeService({ browserSupportsCsp: true });
const start = await service.start(defaultStartDeps());
const promise = start
const { chrome, service } = await start();
const promise = chrome
.getBadge$()
.pipe(toArray())
.toPromise();
start.setBadge({ text: 'foo', tooltip: `foo's tooltip` });
start.setBadge({ text: 'bar', tooltip: `bar's tooltip` });
start.setBadge(undefined);
chrome.setBadge({ text: 'foo', tooltip: `foo's tooltip` });
chrome.setBadge({ text: 'bar', tooltip: `bar's tooltip` });
chrome.setBadge(undefined);
service.stop();
await expect(promise).resolves.toMatchInlineSnapshot(`
Array [
undefined,
Object {
"text": "foo",
"tooltip": "foo's tooltip",
},
Object {
"text": "bar",
"tooltip": "bar's tooltip",
},
undefined,
]
`);
Array [
undefined,
Object {
"text": "foo",
"tooltip": "foo's tooltip",
},
Object {
"text": "bar",
"tooltip": "bar's tooltip",
},
undefined,
]
`);
});
});
describe('breadcrumbs', () => {
it('updates/emits the current set of breadcrumbs', async () => {
const service = new ChromeService({ browserSupportsCsp: true });
const start = await service.start(defaultStartDeps());
const promise = start
const { chrome, service } = await start();
const promise = chrome
.getBreadcrumbs$()
.pipe(toArray())
.toPromise();
start.setBreadcrumbs([{ text: 'foo' }, { text: 'bar' }]);
start.setBreadcrumbs([{ text: 'foo' }]);
start.setBreadcrumbs([{ text: 'bar' }]);
start.setBreadcrumbs([]);
chrome.setBreadcrumbs([{ text: 'foo' }, { text: 'bar' }]);
chrome.setBreadcrumbs([{ text: 'foo' }]);
chrome.setBreadcrumbs([{ text: 'bar' }]);
chrome.setBreadcrumbs([]);
service.stop();
await expect(promise).resolves.toMatchInlineSnapshot(`
Array [
Array [],
Array [
Object {
"text": "foo",
},
Object {
"text": "bar",
},
],
Array [
Object {
"text": "foo",
},
],
Array [
Object {
"text": "bar",
},
],
Array [],
]
`);
Array [
Array [],
Array [
Object {
"text": "foo",
},
Object {
"text": "bar",
},
],
Array [
Object {
"text": "foo",
},
],
Array [
Object {
"text": "bar",
},
],
Array [],
]
`);
});
});
describe('help extension', () => {
it('updates/emits the current help extension', async () => {
const service = new ChromeService({ browserSupportsCsp: true });
const start = await service.start(defaultStartDeps());
const promise = start
const { chrome, service } = await start();
const promise = chrome
.getHelpExtension$()
.pipe(toArray())
.toPromise();
start.setHelpExtension(() => () => undefined);
start.setHelpExtension(undefined);
chrome.setHelpExtension(() => () => undefined);
chrome.setHelpExtension(undefined);
service.stop();
await expect(promise).resolves.toMatchInlineSnapshot(`
Array [
undefined,
[Function],
undefined,
]
`);
Array [
undefined,
[Function],
undefined,
]
`);
});
});
});
describe('stop', () => {
it('completes applicationClass$, isCollapsed$, breadcrumbs$, isVisible$, and brand$ observables', async () => {
const service = new ChromeService({ browserSupportsCsp: true });
const start = await service.start(defaultStartDeps());
const { chrome, service } = await start();
const promise = Rx.combineLatest(
start.getBrand$(),
start.getApplicationClasses$(),
start.getIsCollapsed$(),
start.getBreadcrumbs$(),
start.getIsVisible$(),
start.getHelpExtension$()
chrome.getBrand$(),
chrome.getApplicationClasses$(),
chrome.getIsCollapsed$(),
chrome.getBreadcrumbs$(),
chrome.getIsVisible$(),
chrome.getHelpExtension$()
).toPromise();
service.stop();
@ -378,18 +450,17 @@ describe('stop', () => {
});
it('completes immediately if service already stopped', async () => {
const service = new ChromeService({ browserSupportsCsp: true });
const start = await service.start(defaultStartDeps());
const { chrome, service } = await start();
service.stop();
await expect(
Rx.combineLatest(
start.getBrand$(),
start.getApplicationClasses$(),
start.getIsCollapsed$(),
start.getBreadcrumbs$(),
start.getIsVisible$(),
start.getHelpExtension$()
chrome.getBrand$(),
chrome.getApplicationClasses$(),
chrome.getIsCollapsed$(),
chrome.getBreadcrumbs$(),
chrome.getIsVisible$(),
chrome.getHelpExtension$()
).toPromise()
).resolves.toBe(undefined);
});

View file

@ -18,9 +18,9 @@
*/
import React from 'react';
import { BehaviorSubject, Observable, ReplaySubject } from 'rxjs';
import { BehaviorSubject, Observable, ReplaySubject, combineLatest, of, merge } from 'rxjs';
import { map, takeUntil } from 'rxjs/operators';
import * as Url from 'url';
import { parse } from 'url';
import { i18n } from '@kbn/i18n';
import { IconType, Breadcrumb as EuiBreadcrumb } from '@elastic/eui';
@ -41,11 +41,6 @@ export { ChromeNavControls, ChromeRecentlyAccessed, ChromeDocTitle };
const IS_COLLAPSED_KEY = 'core.chrome.isCollapsed';
function isEmbedParamInHash() {
const { query } = Url.parse(String(window.location.hash).slice(1), true);
return Boolean(query.embed);
}
/** @public */
export interface ChromeBadge {
text: string;
@ -79,6 +74,9 @@ interface StartDeps {
/** @internal */
export class ChromeService {
private isVisible$!: Observable<boolean>;
private appHidden$!: Observable<boolean>;
private toggleHidden$!: BehaviorSubject<boolean>;
private readonly stop$ = new ReplaySubject(1);
private readonly navControls = new NavControlsService();
private readonly navLinks = new NavLinksService();
@ -87,6 +85,38 @@ export class ChromeService {
constructor(private readonly params: ConstructorParams) {}
/**
* These observables allow consumers to toggle the chrome visibility via either:
* 1. Using setIsVisible() to trigger the next chromeHidden$
* 2. Setting `chromeless` when registering an application, which will
* reset the visibility whenever the next application is mounted
* 3. Having "embed" in the query string
*/
private initVisibility(application: StartDeps['application']) {
// Start off the chrome service hidden if "embed" is in the hash query string.
const isEmbedded = 'embed' in parse(location.hash.slice(1), true).query;
this.toggleHidden$ = new BehaviorSubject(isEmbedded);
this.appHidden$ = merge(
// Default the app being hidden to the same value initial value as the chrome visibility
// in case the application service has not emitted an app ID yet, since we want to trigger
// combineLatest below regardless of having an application value yet.
of(isEmbedded),
application.currentAppId$.pipe(
map(
appId =>
!!appId &&
application.availableApps.has(appId) &&
!!application.availableApps.get(appId)!.chromeless
)
)
);
this.isVisible$ = combineLatest(this.appHidden$, this.toggleHidden$).pipe(
map(([appHidden, chromeHidden]) => !(appHidden || chromeHidden)),
takeUntil(this.stop$)
);
}
public async start({
application,
docLinks,
@ -94,11 +124,10 @@ export class ChromeService {
injectedMetadata,
notifications,
}: StartDeps): Promise<InternalChromeStart> {
const FORCE_HIDDEN = isEmbedParamInHash();
this.initVisibility(application);
const appTitle$ = new BehaviorSubject<string>('Kibana');
const brand$ = new BehaviorSubject<ChromeBrand>({});
const isVisible$ = new BehaviorSubject(true);
const isCollapsed$ = new BehaviorSubject(!!localStorage.getItem(IS_COLLAPSED_KEY));
const applicationClasses$ = new BehaviorSubject<Set<string>>(new Set());
const helpExtension$ = new BehaviorSubject<ChromeHelpExtension | undefined>(undefined);
@ -139,10 +168,7 @@ export class ChromeService {
forceAppSwitcherNavigation$={navLinks.getForceAppSwitcherNavigation$()}
helpExtension$={helpExtension$.pipe(takeUntil(this.stop$))}
homeHref={http.basePath.prepend('/app/kibana#/home')}
isVisible$={isVisible$.pipe(
map(visibility => (FORCE_HIDDEN ? false : visibility)),
takeUntil(this.stop$)
)}
isVisible$={this.isVisible$}
kibanaVersion={injectedMetadata.getKibanaVersion()}
legacyMode={injectedMetadata.getLegacyMode()}
navLinks$={navLinks.getNavLinks$()}
@ -166,15 +192,9 @@ export class ChromeService {
);
},
getIsVisible$: () =>
isVisible$.pipe(
map(visibility => (FORCE_HIDDEN ? false : visibility)),
takeUntil(this.stop$)
),
getIsVisible$: () => this.isVisible$,
setIsVisible: (visibility: boolean) => {
isVisible$.next(visibility);
},
setIsVisible: (isVisible: boolean) => this.toggleHidden$.next(!isVisible),
getIsCollapsed$: () => isCollapsed$.pipe(takeUntil(this.stop$)),

View file

@ -16,6 +16,7 @@ import { UserProvidedValues as UserProvidedValues_2 } from 'src/core/server/type
// @public
export interface App extends AppBase {
chromeless?: boolean;
mount: (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise<AppUnmount>;
}

View file

@ -0,0 +1,8 @@
{
"id": "core_plugin_chromeless",
"version": "0.0.1",
"kibanaVersion": "kibana",
"configPath": ["core_plugin_chromeless"],
"server": false,
"ui": true
}

View file

@ -0,0 +1,17 @@
{
"name": "core_plugin_chromeless",
"version": "1.0.0",
"main": "target/test/plugin_functional/plugins/core_plugin_chromeless",
"kibana": {
"version": "kibana",
"templateVersion": "1.0.0"
},
"license": "Apache-2.0",
"scripts": {
"kbn": "node ../../../../scripts/kbn.js",
"build": "rm -rf './target' && tsc"
},
"devDependencies": {
"typescript": "3.5.3"
}
}

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 React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { BrowserRouter as Router, Route } from 'react-router-dom';
import {
EuiPage,
EuiPageBody,
EuiPageContent,
EuiPageContentBody,
EuiPageContentHeader,
EuiPageContentHeaderSection,
EuiPageHeader,
EuiPageHeaderSection,
EuiTitle,
} from '@elastic/eui';
import { AppMountContext, AppMountParameters } from 'kibana/public';
const Home = () => (
<EuiPageBody data-test-subj="chromelessAppHome">
<EuiPageHeader>
<EuiPageHeaderSection>
<EuiTitle size="l">
<h1>Welcome to Chromeless!</h1>
</EuiTitle>
</EuiPageHeaderSection>
</EuiPageHeader>
<EuiPageContent>
<EuiPageContentHeader>
<EuiPageContentHeaderSection>
<EuiTitle>
<h2>Chromeless home page section title</h2>
</EuiTitle>
</EuiPageContentHeaderSection>
</EuiPageContentHeader>
<EuiPageContentBody>Where did all the chrome go?</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
);
const ChromelessApp = ({ basename }: { basename: string; context: AppMountContext }) => (
<Router basename={basename}>
<EuiPage>
<Route path="/" component={Home} />
</EuiPage>
</Router>
);
export const renderApp = (
context: AppMountContext,
{ appBasePath, element }: AppMountParameters
) => {
render(<ChromelessApp basename={appBasePath} context={context} />, element);
return () => unmountComponentAtNode(element);
};

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 { PluginInitializer } from 'kibana/public';
import {
CorePluginChromelessPlugin,
CorePluginChromelessPluginSetup,
CorePluginChromelessPluginStart,
} from './plugin';
export const plugin: PluginInitializer<
CorePluginChromelessPluginSetup,
CorePluginChromelessPluginStart
> = () => new CorePluginChromelessPlugin();

View file

@ -0,0 +1,47 @@
/*
* 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 { Plugin, CoreSetup } from 'kibana/public';
export class CorePluginChromelessPlugin
implements Plugin<CorePluginChromelessPluginSetup, CorePluginChromelessPluginStart> {
public setup(core: CoreSetup, deps: {}) {
core.application.register({
id: 'chromeless',
title: 'Chromeless',
chromeless: true,
async mount(context, params) {
const { renderApp } = await import('./application');
return renderApp(context, params);
},
});
return {
getGreeting() {
return 'Hello from Plugin Chromeless!';
},
};
}
public start() {}
public stop() {}
}
export type CorePluginChromelessPluginSetup = ReturnType<CorePluginChromelessPlugin['setup']>;
export type CorePluginChromelessPluginStart = ReturnType<CorePluginChromelessPlugin['start']>;

View file

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

View file

@ -91,6 +91,18 @@ export default function({ getService, getPageObjects }: PluginFunctionalProvider
await testSubjects.existOrFail('fooAppPageA');
});
it('navigating to chromeless application hides chrome', async () => {
await appsMenu.clickLink('Chromeless');
await loadingScreenNotShown();
expect(await testSubjects.exists('headerGlobalNav')).to.be(false);
});
it('navigating away from chromeless application shows chrome', async () => {
await browser.goBack();
await loadingScreenNotShown();
expect(await testSubjects.exists('headerGlobalNav')).to.be(true);
});
it('can navigate from NP apps to legacy apps', async () => {
await appsMenu.clickLink('Management');
await loadingScreenShown();