mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
* 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:
parent
53af425c62
commit
7bf7f1e50a
14 changed files with 550 additions and 234 deletions
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
chromeless?: boolean;
|
||||
```
|
|
@ -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) => AppUnmount | Promise<AppUnmount></code> | A mount function called when the user navigates to this app's route. |
|
||||
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
|
|
@ -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);
|
||||
* },
|
||||
* });
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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$)),
|
||||
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"id": "core_plugin_chromeless",
|
||||
"version": "0.0.1",
|
||||
"kibanaVersion": "kibana",
|
||||
"configPath": ["core_plugin_chromeless"],
|
||||
"server": false,
|
||||
"ui": true
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
};
|
|
@ -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();
|
|
@ -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']>;
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"extends": "../../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./target",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"index.ts",
|
||||
"public/**/*.ts",
|
||||
"public/**/*.tsx",
|
||||
"../../../../typings/**/*",
|
||||
],
|
||||
"exclude": []
|
||||
}
|
|
@ -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();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue