diff --git a/docs/development/core/public/kibana-plugin-public.app.chromeless.md b/docs/development/core/public/kibana-plugin-public.app.chromeless.md
new file mode 100644
index 0000000000000..dc1e19bab80b2
--- /dev/null
+++ b/docs/development/core/public/kibana-plugin-public.app.chromeless.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-public](./kibana-plugin-public.md) > [App](./kibana-plugin-public.app.md) > [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.
+
+Signature:
+
+```typescript
+chromeless?: boolean;
+```
diff --git a/docs/development/core/public/kibana-plugin-public.app.md b/docs/development/core/public/kibana-plugin-public.app.md
index 60cac357d1fe0..c500c080a5feb 100644
--- a/docs/development/core/public/kibana-plugin-public.app.md
+++ b/docs/development/core/public/kibana-plugin-public.app.md
@@ -16,5 +16,6 @@ export interface App extends AppBase
| Property | Type | Description |
| --- | --- | --- |
+| [chromeless](./kibana-plugin-public.app.chromeless.md) | boolean
| Hide the UI chrome when the application is mounted. Defaults to false
. Takes precedence over chrome service visibility settings. |
| [mount](./kibana-plugin-public.app.mount.md) | (context: AppMountContext, params: AppMountParameters) => AppUnmount | Promise<AppUnmount>
| A mount function called when the user navigates to this app's route. |
diff --git a/docs/development/core/public/kibana-plugin-public.appmountparameters.appbasepath.md b/docs/development/core/public/kibana-plugin-public.appmountparameters.appbasepath.md
index 16c8ffe07fc15..31513bda2e879 100644
--- a/docs/development/core/public/kibana-plugin-public.appmountparameters.appbasepath.md
+++ b/docs/development/core/public/kibana-plugin-public.appmountparameters.appbasepath.md
@@ -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);
+ },
+ });
+ }
}
```
diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts
index 5b1d4affe8840..5be22ea151c32 100644
--- a/src/core/public/application/types.ts
+++ b/src/core/public/application/types.ts
@@ -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;
+
+ /**
+ * 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);
+ * },
+ * });
+ * }
* }
* ```
*
diff --git a/src/core/public/chrome/chrome_service.test.ts b/src/core/public/chrome/chrome_service.test.ts
index 45e94040eeb4a..3390480e56bdd 100644
--- a/src/core/public/chrome/chrome_service.test.ts
+++ b/src/core/public/chrome/chrome_service.test.ts
@@ -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 } = {}) {
+ 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();
+
+ chrome.setIsVisible(true);
+ chrome.setIsVisible(false);
+ chrome.setIsVisible(true);
+ service.stop();
+
+ await expect(promise).resolves.toMatchInlineSnapshot(`
+ 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();
- start.setIsVisible(true);
- start.setIsVisible(false);
- start.setIsVisible(true);
+ currentAppId$.next('alpha');
+ chrome.setIsVisible(true);
service.stop();
await expect(promise).resolves.toMatchInlineSnapshot(`
-Array [
- false,
- false,
- false,
- false,
-]
-`);
+ 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);
});
diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx
index a5532faec19ed..e686f03413dd5 100644
--- a/src/core/public/chrome/chrome_service.tsx
+++ b/src/core/public/chrome/chrome_service.tsx
@@ -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;
+ private appHidden$!: Observable;
+ private toggleHidden$!: BehaviorSubject;
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 {
- const FORCE_HIDDEN = isEmbedParamInHash();
+ this.initVisibility(application);
const appTitle$ = new BehaviorSubject('Kibana');
const brand$ = new BehaviorSubject({});
- const isVisible$ = new BehaviorSubject(true);
const isCollapsed$ = new BehaviorSubject(!!localStorage.getItem(IS_COLLAPSED_KEY));
const applicationClasses$ = new BehaviorSubject>(new Set());
const helpExtension$ = new BehaviorSubject(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$)),
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index a596ea394abda..d3ce86d76d7cc 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -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;
}
diff --git a/test/plugin_functional/plugins/core_plugin_chromeless/kibana.json b/test/plugin_functional/plugins/core_plugin_chromeless/kibana.json
new file mode 100644
index 0000000000000..a8a5616627726
--- /dev/null
+++ b/test/plugin_functional/plugins/core_plugin_chromeless/kibana.json
@@ -0,0 +1,8 @@
+{
+ "id": "core_plugin_chromeless",
+ "version": "0.0.1",
+ "kibanaVersion": "kibana",
+ "configPath": ["core_plugin_chromeless"],
+ "server": false,
+ "ui": true
+}
diff --git a/test/plugin_functional/plugins/core_plugin_chromeless/package.json b/test/plugin_functional/plugins/core_plugin_chromeless/package.json
new file mode 100644
index 0000000000000..eff6c1e1f142a
--- /dev/null
+++ b/test/plugin_functional/plugins/core_plugin_chromeless/package.json
@@ -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"
+ }
+}
diff --git a/test/plugin_functional/plugins/core_plugin_chromeless/public/application.tsx b/test/plugin_functional/plugins/core_plugin_chromeless/public/application.tsx
new file mode 100644
index 0000000000000..556a9ca140715
--- /dev/null
+++ b/test/plugin_functional/plugins/core_plugin_chromeless/public/application.tsx
@@ -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 = () => (
+
+
+
+
+ Welcome to Chromeless!
+
+
+
+
+
+
+
+ Chromeless home page section title
+
+
+
+ Where did all the chrome go?
+
+
+);
+
+const ChromelessApp = ({ basename }: { basename: string; context: AppMountContext }) => (
+
+
+
+
+
+);
+
+export const renderApp = (
+ context: AppMountContext,
+ { appBasePath, element }: AppMountParameters
+) => {
+ render(, element);
+
+ return () => unmountComponentAtNode(element);
+};
diff --git a/test/plugin_functional/plugins/core_plugin_chromeless/public/index.ts b/test/plugin_functional/plugins/core_plugin_chromeless/public/index.ts
new file mode 100644
index 0000000000000..6e9959ecbdf9e
--- /dev/null
+++ b/test/plugin_functional/plugins/core_plugin_chromeless/public/index.ts
@@ -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();
diff --git a/test/plugin_functional/plugins/core_plugin_chromeless/public/plugin.tsx b/test/plugin_functional/plugins/core_plugin_chromeless/public/plugin.tsx
new file mode 100644
index 0000000000000..03870410fb334
--- /dev/null
+++ b/test/plugin_functional/plugins/core_plugin_chromeless/public/plugin.tsx
@@ -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 {
+ 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;
+export type CorePluginChromelessPluginStart = ReturnType;
diff --git a/test/plugin_functional/plugins/core_plugin_chromeless/tsconfig.json b/test/plugin_functional/plugins/core_plugin_chromeless/tsconfig.json
new file mode 100644
index 0000000000000..5fcaeafbb0d85
--- /dev/null
+++ b/test/plugin_functional/plugins/core_plugin_chromeless/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../../../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./target",
+ "skipLibCheck": true
+ },
+ "include": [
+ "index.ts",
+ "public/**/*.ts",
+ "public/**/*.tsx",
+ "../../../../typings/**/*",
+ ],
+ "exclude": []
+}
diff --git a/test/plugin_functional/test_suites/core_plugins/applications.ts b/test/plugin_functional/test_suites/core_plugins/applications.ts
index eec2ec019a515..138e20b987761 100644
--- a/test/plugin_functional/test_suites/core_plugins/applications.ts
+++ b/test/plugin_functional/test_suites/core_plugins/applications.ts
@@ -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();