-
Notifications
You must be signed in to change notification settings - Fork 8.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[core/public/chrome] migrate controls, theme, and visibility apis (#2…
…2987) (#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
- Loading branch information
Spencer
authored
Oct 20, 2018
1 parent
27b86eb
commit e33cba3
Showing
17 changed files
with
908 additions
and
187 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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']>; |
Oops, something went wrong.