[core/public/chrome] migrate controls, theme, and visibility apis (#22987) (#24308)

* [core/public/chrome] migrate controls, theme, and visibility apis

* [core/public] stop uiSettings service

* [core/public/chrome] test that observables stop immedaiately after stop()

* fix typos

* [core/public/legacyPlatform] test globalNavState init

* [ui/chrome] don't pass extra params

* [core/public/chrome] test for dedupe-handling

* [ui/chrome/theme] test with different values for logo and smallLogo
This commit is contained in:
Spencer 2018-10-19 20:30:03 -07:00 committed by GitHub
parent 27b86ebb05
commit e33cba34b7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 909 additions and 188 deletions

View file

@ -0,0 +1,239 @@
/*
* 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 * as Rx from 'rxjs';
import { toArray } from 'rxjs/operators';
const store = new Map();
(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)),
};
import { ChromeService } from './chrome_service';
beforeEach(() => {
store.clear();
});
describe('start', () => {
describe('brand', () => {
it('updates/emits the brand as it changes', async () => {
const service = new ChromeService();
const start = service.start();
const promise = start
.getBrand$()
.pipe(toArray())
.toPromise();
start.setBrand({
logo: 'big logo',
smallLogo: 'not so big logo',
});
start.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,
},
]
`);
});
});
describe('visibility', () => {
it('updates/emits the visibility', async () => {
const service = new ChromeService();
const start = service.start();
const promise = start
.getIsVisible$()
.pipe(toArray())
.toPromise();
start.setIsVisible(true);
start.setIsVisible(false);
start.setIsVisible(true);
service.stop();
await expect(promise).resolves.toMatchInlineSnapshot(`
Array [
true,
true,
false,
true,
]
`);
});
it('always emits false if embed query string is in hash when started', async () => {
window.history.pushState(undefined, '', '#/home?a=b&embed=true');
const service = new ChromeService();
const start = service.start();
const promise = start
.getIsVisible$()
.pipe(toArray())
.toPromise();
start.setIsVisible(true);
start.setIsVisible(false);
start.setIsVisible(true);
service.stop();
await expect(promise).resolves.toMatchInlineSnapshot(`
Array [
false,
false,
false,
false,
]
`);
});
});
describe('is collapsed', () => {
it('updates/emits isCollapsed', async () => {
const service = new ChromeService();
const start = service.start();
const promise = start
.getIsCollapsed$()
.pipe(toArray())
.toPromise();
start.setIsCollapsed(true);
start.setIsCollapsed(false);
start.setIsCollapsed(true);
service.stop();
await expect(promise).resolves.toMatchInlineSnapshot(`
Array [
false,
true,
false,
true,
]
`);
});
it('only stores true in localStorage', async () => {
const service = new ChromeService();
const start = service.start();
start.setIsCollapsed(true);
expect(store.size).toBe(1);
start.setIsCollapsed(false);
expect(store.size).toBe(0);
});
});
describe('application classes', () => {
it('updates/emits the application classes', async () => {
const service = new ChromeService();
const start = service.start();
const promise = start
.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');
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",
],
]
`);
});
});
});
describe('stop', () => {
it('completes applicationClass$, isCollapsed$, isVisible$, and brand$ observables', async () => {
const service = new ChromeService();
const start = service.start();
const promise = Rx.combineLatest(
start.getBrand$(),
start.getApplicationClasses$(),
start.getIsCollapsed$(),
start.getIsVisible$()
).toPromise();
service.stop();
await promise;
});
it('completes immediately if service already stopped', async () => {
const service = new ChromeService();
const start = service.start();
service.stop();
await expect(
Rx.combineLatest(
start.getBrand$(),
start.getApplicationClasses$(),
start.getIsCollapsed$(),
start.getIsVisible$()
).toPromise()
).resolves.toBe(undefined);
});
});

View file

@ -0,0 +1,146 @@
/*
* 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 * as Url from 'url';
import * as Rx from 'rxjs';
import { map, takeUntil } from 'rxjs/operators';
const IS_COLLAPSED_KEY = 'core.chrome.isCollapsed';
function isEmbedParamInHash() {
const { query } = Url.parse(String(window.location.hash).slice(1), true);
return Boolean(query.embed);
}
export interface Brand {
logo?: string;
smallLogo?: string;
}
export class ChromeService {
private readonly stop$ = new Rx.ReplaySubject(1);
public start() {
const FORCE_HIDDEN = isEmbedParamInHash();
const brand$ = new Rx.BehaviorSubject<Brand>({});
const isVisible$ = new Rx.BehaviorSubject(true);
const isCollapsed$ = new Rx.BehaviorSubject(!!localStorage.getItem(IS_COLLAPSED_KEY));
const applicationClasses$ = new Rx.BehaviorSubject<Set<string>>(new Set());
return {
/**
* Set the brand configuration. Normally the `logo` property will be rendered as the
* CSS background for the home link in the chrome navigation, but when the page is
* rendered in a small window the `smallLogo` will be used and rendered at about
* 45px wide.
*
* example:
*
* chrome.setBrand({
* logo: 'url(/plugins/app/logo.png) center no-repeat'
* smallLogo: 'url(/plugins/app/logo-small.png) center no-repeat'
* })
*
*/
setBrand: (brand: Brand) => {
brand$.next(
Object.freeze({
logo: brand.logo,
smallLogo: brand.smallLogo,
})
);
},
/**
* Get an observable of the current brand information.
*/
getBrand$: () => brand$.pipe(takeUntil(this.stop$)),
/**
* Set the temporary visibility for the chrome. This does nothing if the chrome is hidden
* by default and should be used to hide the chrome for things like full-screen modes
* with an exit button.
*/
setIsVisible: (visibility: boolean) => {
isVisible$.next(visibility);
},
/**
* Get an observable of the current visibility state of the chrome.
*/
getIsVisible$: () =>
isVisible$.pipe(
map(visibility => (FORCE_HIDDEN ? false : visibility)),
takeUntil(this.stop$)
),
/**
* Set the collapsed state of the chrome navigation.
*/
setIsCollapsed: (isCollapsed: boolean) => {
isCollapsed$.next(isCollapsed);
if (isCollapsed) {
localStorage.setItem(IS_COLLAPSED_KEY, 'true');
} else {
localStorage.removeItem(IS_COLLAPSED_KEY);
}
},
/**
* Get an observable of the current collapsed state of the chrome.
*/
getIsCollapsed$: () => isCollapsed$.pipe(takeUntil(this.stop$)),
/**
* Add a className that should be set on the application container.
*/
addApplicationClass: (className: string) => {
const update = new Set([...applicationClasses$.getValue()]);
update.add(className);
applicationClasses$.next(update);
},
/**
* Remove a className added with `addApplicationClass()`. If className is unknown it is ignored.
*/
removeApplicationClass: (className: string) => {
const update = new Set([...applicationClasses$.getValue()]);
update.delete(className);
applicationClasses$.next(update);
},
/**
* Get the current set of classNames that will be set on the application container.
*/
getApplicationClasses$: () =>
applicationClasses$.pipe(
map(set => [...set]),
takeUntil(this.stop$)
),
};
}
public stop() {
this.stop$.next();
}
}
export type ChromeStartContract = ReturnType<ChromeService['start']>;

View file

@ -0,0 +1,20 @@
/*
* 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.
*/
export { ChromeService, ChromeStartContract, Brand } from './chrome_service';

View file

@ -18,6 +18,7 @@
*/
import { BasePathService } from './base_path';
import { ChromeService } from './chrome';
import { FatalErrorsService } from './fatal_errors';
import { InjectedMetadataService } from './injected_metadata';
import { LegacyPlatformService } from './legacy_platform';
@ -92,11 +93,21 @@ const MockUiSettingsService = jest.fn<UiSettingsService>(function _MockNotificat
this: any
) {
this.start = jest.fn().mockReturnValue(mockUiSettingsContract);
this.stop = jest.fn();
});
jest.mock('./ui_settings', () => ({
UiSettingsService: MockUiSettingsService,
}));
const mockChromeStartContract = {};
const MockChromeService = jest.fn<ChromeService>(function _MockNotificationsService(this: any) {
this.start = jest.fn().mockReturnValue(mockChromeStartContract);
this.stop = jest.fn();
});
jest.mock('./chrome', () => ({
ChromeService: MockChromeService,
}));
import { CoreSystem } from './core_system';
jest.spyOn(CoreSystem.prototype, 'stop');
@ -124,6 +135,7 @@ describe('constructor', () => {
expect(MockLoadingCountService).toHaveBeenCalledTimes(1);
expect(MockBasePathService).toHaveBeenCalledTimes(1);
expect(MockUiSettingsService).toHaveBeenCalledTimes(1);
expect(MockChromeService).toHaveBeenCalledTimes(1);
});
it('passes injectedMetadata param to InjectedMetadataService', () => {
@ -231,6 +243,28 @@ describe('#stop', () => {
expect(loadingCountService.stop).toHaveBeenCalled();
});
it('calls chrome.stop()', () => {
const coreSystem = new CoreSystem({
...defaultCoreSystemParams,
});
const [chromeService] = MockChromeService.mock.instances;
expect(chromeService.stop).not.toHaveBeenCalled();
coreSystem.stop();
expect(chromeService.stop).toHaveBeenCalled();
});
it('calls uiSettings.stop()', () => {
const coreSystem = new CoreSystem({
...defaultCoreSystemParams,
});
const [uiSettings] = MockUiSettingsService.mock.instances;
expect(uiSettings.stop).not.toHaveBeenCalled();
coreSystem.stop();
expect(uiSettings.stop).toHaveBeenCalled();
});
it('clears the rootDomElement', () => {
const rootDomElement = document.createElement('div');
const coreSystem = new CoreSystem({
@ -312,6 +346,13 @@ describe('#start()', () => {
expect(mockInstance.start).toHaveBeenCalledTimes(1);
expect(mockInstance.start).toHaveBeenCalledWith();
});
it('calls chrome#start()', () => {
startCore();
const [mockInstance] = MockChromeService.mock.instances;
expect(mockInstance.start).toHaveBeenCalledTimes(1);
expect(mockInstance.start).toHaveBeenCalledWith();
});
});
describe('LegacyPlatform targetDomElement', () => {

View file

@ -20,6 +20,7 @@
import './core.css';
import { BasePathService } from './base_path';
import { ChromeService } from './chrome';
import { FatalErrorsService } from './fatal_errors';
import { InjectedMetadataParams, InjectedMetadataService } from './injected_metadata';
import { LegacyPlatformParams, LegacyPlatformService } from './legacy_platform';
@ -48,6 +49,7 @@ export class CoreSystem {
private readonly loadingCount: LoadingCountService;
private readonly uiSettings: UiSettingsService;
private readonly basePath: BasePathService;
private readonly chrome: ChromeService;
private readonly rootDomElement: HTMLElement;
private readonly notificationsTargetDomElement: HTMLDivElement;
@ -78,6 +80,7 @@ export class CoreSystem {
this.loadingCount = new LoadingCountService();
this.basePath = new BasePathService();
this.uiSettings = new UiSettingsService();
this.chrome = new ChromeService();
this.legacyPlatformTargetDomElement = document.createElement('div');
this.legacyPlatform = new LegacyPlatformService({
@ -106,6 +109,8 @@ export class CoreSystem {
injectedMetadata,
basePath,
});
const chrome = this.chrome.start();
this.legacyPlatform.start({
injectedMetadata,
fatalErrors,
@ -113,6 +118,7 @@ export class CoreSystem {
loadingCount,
basePath,
uiSettings,
chrome,
});
} catch (error) {
this.fatalErrors.add(error);
@ -123,6 +129,8 @@ export class CoreSystem {
this.legacyPlatform.stop();
this.notifications.stop();
this.loadingCount.stop();
this.uiSettings.stop();
this.chrome.stop();
this.rootDomElement.textContent = '';
}
}

View file

@ -9,6 +9,9 @@ Array [
"ui/chrome/api/base_path",
"ui/chrome/api/ui_settings",
"ui/chrome/api/injected_vars",
"ui/chrome/api/controls",
"ui/chrome/api/theme",
"ui/chrome/services/global_nav_state",
"ui/chrome",
"legacy files",
]
@ -23,6 +26,9 @@ Array [
"ui/chrome/api/base_path",
"ui/chrome/api/ui_settings",
"ui/chrome/api/injected_vars",
"ui/chrome/api/controls",
"ui/chrome/api/theme",
"ui/chrome/services/global_nav_state",
"ui/test_harness",
"legacy files",
]

View file

@ -94,6 +94,30 @@ jest.mock('ui/chrome/api/injected_vars', () => {
};
});
const mockChromeControlsInit = jest.fn();
jest.mock('ui/chrome/api/controls', () => {
mockLoadOrder.push('ui/chrome/api/controls');
return {
__newPlatformInit__: mockChromeControlsInit,
};
});
const mockChromeThemeInit = jest.fn();
jest.mock('ui/chrome/api/theme', () => {
mockLoadOrder.push('ui/chrome/api/theme');
return {
__newPlatformInit__: mockChromeThemeInit,
};
});
const mockGlobalNavStateInit = jest.fn();
jest.mock('ui/chrome/services/global_nav_state', () => {
mockLoadOrder.push('ui/chrome/services/global_nav_state');
return {
__newPlatformInit__: mockGlobalNavStateInit,
};
});
import { LegacyPlatformService } from './legacy_platform_service';
const fatalErrorsStartContract = {} as any;
@ -118,6 +142,7 @@ const basePathStartContract = {
};
const uiSettingsStartContract: any = {};
const chromeStartContract: any = {};
const defaultParams = {
targetDomElement: document.createElement('div'),
@ -133,6 +158,7 @@ const defaultStartDeps = {
loadingCount: loadingCountStartContract,
basePath: basePathStartContract,
uiSettings: uiSettingsStartContract,
chrome: chromeStartContract,
};
afterEach(() => {
@ -224,6 +250,39 @@ describe('#start()', () => {
expect(mockInjectedVarsInit).toHaveBeenCalledWith(injectedMetadataStartContract);
});
it('passes chrome service to ui/chrome/api/controls', () => {
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
});
legacyPlatform.start(defaultStartDeps);
expect(mockChromeControlsInit).toHaveBeenCalledTimes(1);
expect(mockChromeControlsInit).toHaveBeenCalledWith(chromeStartContract);
});
it('passes chrome service to ui/chrome/api/theme', () => {
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
});
legacyPlatform.start(defaultStartDeps);
expect(mockChromeThemeInit).toHaveBeenCalledTimes(1);
expect(mockChromeThemeInit).toHaveBeenCalledWith(chromeStartContract);
});
it('passes chrome service to ui/chrome/api/global_nav_state', () => {
const legacyPlatform = new LegacyPlatformService({
...defaultParams,
});
legacyPlatform.start(defaultStartDeps);
expect(mockGlobalNavStateInit).toHaveBeenCalledTimes(1);
expect(mockGlobalNavStateInit).toHaveBeenCalledWith(chromeStartContract);
});
describe('useLegacyTestHarness = false', () => {
it('passes the targetDomElement to ui/chrome', () => {
const legacyPlatform = new LegacyPlatformService({

View file

@ -19,6 +19,7 @@
import angular from 'angular';
import { BasePathStartContract } from '../base_path';
import { ChromeStartContract } from '../chrome';
import { FatalErrorsStartContract } from '../fatal_errors';
import { InjectedMetadataStartContract } from '../injected_metadata';
import { LoadingCountStartContract } from '../loading_count';
@ -32,6 +33,7 @@ interface Deps {
loadingCount: LoadingCountStartContract;
basePath: BasePathStartContract;
uiSettings: UiSettingsClient;
chrome: ChromeStartContract;
}
export interface LegacyPlatformParams {
@ -57,6 +59,7 @@ export class LegacyPlatformService {
loadingCount,
basePath,
uiSettings,
chrome,
}: Deps) {
// Inject parts of the new platform into parts of the legacy platform
// so that legacy APIs/modules can mimic their new platform counterparts
@ -67,6 +70,9 @@ export class LegacyPlatformService {
require('ui/chrome/api/base_path').__newPlatformInit__(basePath);
require('ui/chrome/api/ui_settings').__newPlatformInit__(uiSettings);
require('ui/chrome/api/injected_vars').__newPlatformInit__(injectedMetadata);
require('ui/chrome/api/controls').__newPlatformInit__(chrome);
require('ui/chrome/api/theme').__newPlatformInit__(chrome);
require('ui/chrome/services/global_nav_state').__newPlatformInit__(chrome);
// Load the bootstrap module before loading the legacy platform files so that
// the bootstrap module can modify the environment a bit first

View file

@ -1,62 +0,0 @@
/*
* 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 _ from 'lodash';
// eslint-disable-next-line @elastic/kibana-custom/no-default-export
export default function (chrome, internals) {
/**
* ui/chrome Controls API
*
* Exposes controls for the Kibana chrome
*
* Visible
* determines if the Kibana chrome should be displayed
*/
let permanentlyHideChrome = false;
internals.permanentlyHideChrome = () => {
permanentlyHideChrome = true;
internals.visible = false;
};
chrome.getIsChromePermanentlyHidden = () => {
return permanentlyHideChrome;
};
/**
* @param {boolean} display - should the chrome be displayed
* @return {chrome}
*/
chrome.setVisible = function (display) {
if (permanentlyHideChrome) {
return chrome;
}
internals.visible = Boolean(display);
return chrome;
};
/**
* @return {boolean} - display state of the chrome
*/
chrome.getVisible = function () {
if (_.isUndefined(internals.visible)) return !permanentlyHideChrome;
return internals.visible;
};
}

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 * as Rx from 'rxjs';
import { __newPlatformInit__, initChromeControlsApi } from './controls';
const newPlatformChrome = {
setIsVisible: jest.fn(),
getIsVisible$: jest.fn(),
};
__newPlatformInit__(newPlatformChrome as any);
function setup() {
const isVisible$ = new Rx.BehaviorSubject(true);
newPlatformChrome.getIsVisible$.mockReturnValue(isVisible$);
const chrome: any = {};
initChromeControlsApi(chrome);
return { chrome, isVisible$ };
}
afterEach(() => {
jest.resetAllMocks();
});
describe('setVisible', () => {
it('passes the visibility to the newPlatform', () => {
const { chrome } = setup();
chrome.setVisible(true);
chrome.setVisible(false);
chrome.setVisible(false);
expect(newPlatformChrome.setIsVisible.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
true,
],
Array [
false,
],
Array [
false,
],
]
`);
});
});
describe('getVisible', () => {
it('returns a the cached value emitted by the newPlatformChrome', () => {
const { chrome, isVisible$ } = setup();
isVisible$.next(true);
expect(chrome.getVisible()).toBe(true);
isVisible$.next(false);
expect(chrome.getVisible()).toBe(false);
});
});

View file

@ -0,0 +1,53 @@
/*
* 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 * as Rx from 'rxjs';
import { ChromeStartContract } from '../../../../core/public/chrome';
let newPlatformChrome: ChromeStartContract;
export function __newPlatformInit__(instance: ChromeStartContract) {
if (newPlatformChrome) {
throw new Error('ui/chrome/api/controls is already initialized');
}
newPlatformChrome = instance;
}
export function initChromeControlsApi(chrome: { [key: string]: any }) {
// cache of chrome visibility state
const visible$ = new Rx.BehaviorSubject(false);
newPlatformChrome.getIsVisible$().subscribe(visible$);
/**
* Set the temporary visibility for the chrome. This does nothing if the chrome is hidden
* by default and should be used to hide the chrome for things like full-screen modes
* with an exit button.
*/
chrome.setVisible = (visibility: boolean) => {
newPlatformChrome.setIsVisible(visibility);
return chrome;
};
/**
* Get the current visibility state of the chrome. Note that this drives the UI so it
* might be incorrect in the moments just before the UI is updated.
*/
chrome.getVisible = () => visible$.getValue();
}

View file

@ -1,100 +0,0 @@
/*
* 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 _ from 'lodash';
// eslint-disable-next-line @elastic/kibana-custom/no-default-export
export default function (chrome, internals) {
/**
* ui/chrome Theme API
*
* Logo
* Set the background for the logo and small logo in the navbar.
* When the app is in the "small" category, a modified version of the
* logo is displayed that is 45px wide.
* e.g., 'url(/plugins/app/logo.png) center no-repeat'
*
* Brand
* Similar to a logo, but is just text with styles to make it stick out.
*/
/**
* @param {string|object} item - brand key to set, or object to apply
* @param {mixed} val - value to put on the brand item
* @return {chrome}
*/
chrome.setBrand = function (item, val) {
internals.brand = internals.brand || {};
// allow objects to be passed in
if (_.isPlainObject(item)) {
internals.brand = _.clone(item);
} else {
internals.brand[item] = val;
}
return chrome;
};
/**
* @return {string} - the brand text
*/
chrome.getBrand = function (item) {
if (!internals.brand) return;
return internals.brand[item];
};
/**
* Adds a class to the application node
* @param {string} - the class name to add
* @return {chrome}
*/
chrome.addApplicationClass = function (val) {
let classes = internals.applicationClasses || [];
classes.push(val);
classes = _.uniq(classes);
internals.applicationClasses = classes;
return chrome;
};
/**
* Removes a class from the application node. Note: this only
* removes classes that were added via the addApplicationClass method
* @param {string|[string]} - class or classes to be removed
* @return {chrome}
*/
chrome.removeApplicationClass = function (val) {
const classesToRemove = [].concat(val || []);
const classes = internals.applicationClasses || [];
_.pull(classes, ...classesToRemove);
internals.applicationClasses = classes;
return chrome;
};
/**
* @return {string} - a space delimited string of the classes added by the
* addApplicationClass method
*/
chrome.getApplicationClasses = function () {
return internals.applicationClasses.join(' ');
};
}

View file

@ -0,0 +1,153 @@
/*
* 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 * as Rx from 'rxjs';
import { __newPlatformInit__, initChromeThemeApi } from './theme';
const newPlatformChrome = {
setBrand: jest.fn(),
getBrand$: jest.fn(),
addApplicationClass: jest.fn(),
removeApplicationClass: jest.fn(),
getApplicationClasses$: jest.fn(),
};
__newPlatformInit__(newPlatformChrome as any);
function setup() {
const brand$ = new Rx.BehaviorSubject({ logo: 'foo', smallLogo: 'foo' });
newPlatformChrome.getBrand$.mockReturnValue(brand$);
const applicationClasses$ = new Rx.BehaviorSubject([] as string[]);
newPlatformChrome.getApplicationClasses$.mockReturnValue(applicationClasses$);
const chrome: any = {};
initChromeThemeApi(chrome);
return { chrome, brand$, applicationClasses$ };
}
afterEach(() => {
jest.resetAllMocks();
});
describe('setBrand', () => {
it('proxies to newPlatformChrome', () => {
const { chrome } = setup();
chrome.setBrand({
logo: 'foo.svg',
smallLogo: 'smallFoo.svg',
});
chrome.setBrand({
logo: 'baz',
});
expect(newPlatformChrome.setBrand.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Object {
"logo": "foo.svg",
"smallLogo": "smallFoo.svg",
},
],
Array [
Object {
"logo": "baz",
},
],
]
`);
});
});
describe('getBrand', () => {
it('returns named properties from cached values emitted from newPlatformChrome', () => {
const { chrome, brand$ } = setup();
expect(chrome.getBrand('logo')).toBe('foo');
expect(chrome.getBrand('smallLogo')).toBe('foo');
expect(chrome.getBrand()).toBe(undefined);
brand$.next({
logo: 'bar.svg',
smallLogo: 'smallBar.svg',
});
expect(chrome.getBrand('logo')).toBe('bar.svg');
expect(chrome.getBrand('smallLogo')).toBe('smallBar.svg');
expect(chrome.getBrand()).toBe(undefined);
});
});
describe('addApplicationClass', () => {
it('proxies each class as a separate argument to newPlatformChrome', () => {
const { chrome } = setup();
chrome.addApplicationClass('foo');
chrome.addApplicationClass(['bar', 'baz']);
chrome.addApplicationClass([]);
expect(newPlatformChrome.addApplicationClass.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
"foo",
],
Array [
"bar",
],
Array [
"baz",
],
]
`);
});
});
describe('removeApplicationClass', () => {
it('proxies each class as a separate argument to newPlatformChrome', () => {
const { chrome } = setup();
chrome.removeApplicationClass('foo');
chrome.removeApplicationClass(['bar', 'baz']);
chrome.removeApplicationClass([]);
expect(newPlatformChrome.removeApplicationClass.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
"foo",
],
Array [
"bar",
],
Array [
"baz",
],
]
`);
});
});
describe('getApplicationClasses', () => {
it('returns cached values emitted from newPlatformChrome as a single string', () => {
const { chrome, applicationClasses$ } = setup();
expect(chrome.getApplicationClasses()).toBe('');
applicationClasses$.next(['foo', 'bar']);
expect(chrome.getApplicationClasses()).toBe('foo bar');
applicationClasses$.next(['bar']);
expect(chrome.getApplicationClasses()).toBe('bar');
});
});

View file

@ -0,0 +1,76 @@
/*
* 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 * as Rx from 'rxjs';
import { Brand, ChromeStartContract } from '../../../../core/public/chrome';
let newPlatformChrome: ChromeStartContract;
export function __newPlatformInit__(instance: ChromeStartContract) {
if (newPlatformChrome) {
throw new Error('ui/chrome/api/theme is already initialized');
}
newPlatformChrome = instance;
}
export function initChromeThemeApi(chrome: { [key: string]: any }) {
const brandCache$ = new Rx.BehaviorSubject<Brand>({});
newPlatformChrome.getBrand$().subscribe(brandCache$);
const applicationClassesCache$ = new Rx.BehaviorSubject<string[]>([]);
newPlatformChrome.getApplicationClasses$().subscribe(applicationClassesCache$);
chrome.setBrand = (brand: Brand) => {
newPlatformChrome.setBrand(brand);
return chrome;
};
chrome.getBrand = (key: keyof Brand) => {
return brandCache$.getValue()[key];
};
chrome.addApplicationClass = (classNames: string | string[] = []) => {
if (typeof classNames === 'string') {
classNames = [classNames];
}
for (const className of classNames) {
newPlatformChrome.addApplicationClass(className);
}
return chrome;
};
chrome.removeApplicationClass = (classNames: string | string[]) => {
if (typeof classNames === 'string') {
classNames = [classNames];
}
for (const className of classNames) {
newPlatformChrome.removeApplicationClass(className);
}
return chrome;
};
chrome.getApplicationClasses = () => {
return applicationClassesCache$.getValue().join(' ');
};
}

View file

@ -33,10 +33,10 @@ import './services';
import { initAngularApi } from './api/angular';
import appsApi from './api/apps';
import controlsApi from './api/controls';
import { initChromeControlsApi } from './api/controls';
import { initChromeNavApi } from './api/nav';
import templateApi from './api/template';
import themeApi from './api/theme';
import { initChromeThemeApi } from './api/theme';
import { initChromeXsrfApi } from './api/xsrf';
import { initUiSettingsApi } from './api/ui_settings';
import { initLoadingCountApi } from './api/loading_count';
@ -69,9 +69,9 @@ initChromeInjectedVarsApi(chrome);
initChromeNavApi(chrome, internals);
initLoadingCountApi(chrome, internals);
initAngularApi(chrome, internals);
controlsApi(chrome, internals);
initChromeControlsApi(chrome);
templateApi(chrome, internals);
themeApi(chrome, internals);
initChromeThemeApi(chrome);
const waitForBootstrap = new Promise(resolve => {
chrome.bootstrap = function (targetDomElement) {

View file

@ -57,16 +57,10 @@ export function kbnChromeProvider(chrome, internals) {
},
controllerAs: 'chrome',
controller($scope, $rootScope, $location, $http, Private, config) {
controller($scope, $rootScope, Private, config) {
config.watch('k7design', (val) => $scope.k7design = val);
const getUnhashableStates = Private(getUnhashableStatesProvider);
// are we showing the embedded version of the chrome?
if (Boolean($location.search().embed)) {
internals.permanentlyHideChrome();
}
const subUrlRouteFilter = Private(SubUrlRouteFilterProvider);
function updateSubUrls() {

View file

@ -17,25 +17,33 @@
* under the License.
*/
import { distinctUntilChanged } from 'rxjs/operators';
import { uiModules } from '../../modules';
uiModules.get('kibana')
.service('globalNavState', (localStorage, $rootScope) => {
return {
isOpen: () => {
const isOpen = localStorage.get('kibana.isGlobalNavOpen');
if (isOpen === null) {
// The global nav should default to being open for the initial experience.
return true;
}
return isOpen;
},
let newPlatformChrome;
export function __newPlatformInit__(instance) {
if (newPlatformChrome) {
throw new Error('ui/chrome/global_nav_state is already initialized');
}
setOpen: isOpen => {
localStorage.set('kibana.isGlobalNavOpen', isOpen);
newPlatformChrome = instance;
}
uiModules.get('kibana')
.service('globalNavState', ($rootScope) => {
let isOpen = false;
newPlatformChrome.getIsCollapsed$().pipe(distinctUntilChanged()).subscribe(isCollapsed => {
$rootScope.$evalAsync(() => {
isOpen = !isCollapsed;
$rootScope.$broadcast('globalNavState:change');
return isOpen;
});
});
return {
isOpen: () => isOpen,
setOpen: newValue => {
newPlatformChrome.setIsCollapsed(!newValue);
}
};
});