Skip to content

Commit

Permalink
[core/public/chrome] migrate controls, theme, and visibility apis
Browse files Browse the repository at this point in the history
  • Loading branch information
spalger committed Sep 17, 2018
1 parent 20b9cba commit dd151d6
Show file tree
Hide file tree
Showing 17 changed files with 879 additions and 186 deletions.
215 changes: 215 additions & 0 deletions src/core/public/chrome/chrome_service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
/*
* 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, 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('bar');
start.addApplicationClass('baz');
start.removeApplicationClass('bar');
start.removeApplicationClass('foo');
service.stop();

await expect(promise).resolves.toMatchInlineSnapshot(`
Array [
Array [],
Array [
"foo",
],
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;
});
});
147 changes: 147 additions & 0 deletions src/core/public/chrome/chrome_service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
* 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';

import { LocalStore } from './local_store';

const isCollapsedStore = new LocalStore('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(!!isCollapsedStore.get());
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 renderd
* 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 visiblity 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) {
isCollapsedStore.set('true');
} else {
isCollapsedStore.delete();
}
},

/**
* 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']>;
20 changes: 20 additions & 0 deletions src/core/public/chrome/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Loading

0 comments on commit dd151d6

Please sign in to comment.