Skip to content

Commit

Permalink
[7.x] Display global loading bar while applications are mounting (ela…
Browse files Browse the repository at this point in the history
  • Loading branch information
joshdover committed May 4, 2020
1 parent af89ad7 commit 8a1d4d0
Show file tree
Hide file tree
Showing 9 changed files with 242 additions and 31 deletions.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

77 changes: 73 additions & 4 deletions src/core/public/application/application_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import { createElement } from 'react';
import { BehaviorSubject, Subject } from 'rxjs';
import { bufferCount, take, takeUntil } from 'rxjs/operators';
import { shallow } from 'enzyme';
import { shallow, mount } from 'enzyme';

import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock';
import { contextServiceMock } from '../context/context_service.mock';
Expand All @@ -30,6 +30,7 @@ import { MockCapabilitiesService, MockHistory } from './application_service.test
import { MockLifecycle } from './test_types';
import { ApplicationService } from './application_service';
import { App, AppNavLinkStatus, AppStatus, AppUpdater, LegacyApp } from './types';
import { act } from 'react-dom/test-utils';

const createApp = (props: Partial<App>): App => {
return {
Expand Down Expand Up @@ -452,9 +453,9 @@ describe('#setup()', () => {
const container = setupDeps.context.createContextContainer.mock.results[0].value;
const pluginId = Symbol();

const mount = () => () => undefined;
registerMountContext(pluginId, 'test' as any, mount);
expect(container.registerContext).toHaveBeenCalledWith(pluginId, 'test', mount);
const appMount = () => () => undefined;
registerMountContext(pluginId, 'test' as any, appMount);
expect(container.registerContext).toHaveBeenCalledWith(pluginId, 'test', appMount);
});
});

Expand Down Expand Up @@ -809,6 +810,74 @@ describe('#start()', () => {
`);
});

it('updates httpLoadingCount$ while mounting', async () => {
// Use a memory history so that mounting the component will work
const { createMemoryHistory } = jest.requireActual('history');
const history = createMemoryHistory();
setupDeps.history = history;

const flushPromises = () => new Promise(resolve => setImmediate(resolve));
// Create an app and a promise that allows us to control when the app completes mounting
const createWaitingApp = (props: Partial<App>): [App, () => void] => {
let finishMount: () => void;
const mountPromise = new Promise(resolve => (finishMount = resolve));
const app = {
id: 'some-id',
title: 'some-title',
mount: async () => {
await mountPromise;
return () => undefined;
},
...props,
};

return [app, finishMount!];
};

// Create some dummy applications
const { register } = service.setup(setupDeps);
const [alphaApp, finishAlphaMount] = createWaitingApp({ id: 'alpha' });
const [betaApp, finishBetaMount] = createWaitingApp({ id: 'beta' });
register(Symbol(), alphaApp);
register(Symbol(), betaApp);

const { navigateToApp, getComponent } = await service.start(startDeps);
const httpLoadingCount$ = startDeps.http.addLoadingCountSource.mock.calls[0][0];
const stop$ = new Subject();
const currentLoadingCount$ = new BehaviorSubject(0);
httpLoadingCount$.pipe(takeUntil(stop$)).subscribe(currentLoadingCount$);
const loadingPromise = httpLoadingCount$.pipe(bufferCount(5), takeUntil(stop$)).toPromise();
mount(getComponent()!);

await act(() => navigateToApp('alpha'));
expect(currentLoadingCount$.value).toEqual(1);
await act(async () => {
finishAlphaMount();
await flushPromises();
});
expect(currentLoadingCount$.value).toEqual(0);

await act(() => navigateToApp('beta'));
expect(currentLoadingCount$.value).toEqual(1);
await act(async () => {
finishBetaMount();
await flushPromises();
});
expect(currentLoadingCount$.value).toEqual(0);

stop$.next();
const loadingCounts = await loadingPromise;
expect(loadingCounts).toMatchInlineSnapshot(`
Array [
0,
1,
0,
1,
0,
]
`);
});

it('sets window.location.href when navigating to legacy apps', async () => {
setupDeps.http = httpServiceMock.createSetupContract({ basePath: '/test' });
setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(true);
Expand Down
4 changes: 4 additions & 0 deletions src/core/public/application/application_service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,9 @@ export class ApplicationService {
throw new Error('ApplicationService#setup() must be invoked before start.');
}

const httpLoadingCount$ = new BehaviorSubject(0);
http.addLoadingCountSource(httpLoadingCount$);

this.registrationClosed = true;
window.addEventListener('beforeunload', this.onBeforeUnload);

Expand Down Expand Up @@ -303,6 +306,7 @@ export class ApplicationService {
mounters={availableMounters}
appStatuses$={applicationStatuses$}
setAppLeaveHandler={this.setAppLeaveHandler}
setIsMounting={isMounting => httpLoadingCount$.next(isMounting ? 1 : 0)}
/>
);
},
Expand Down
30 changes: 17 additions & 13 deletions src/core/public/application/integration_tests/router.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe('AppContainer', () => {
};
const mockMountersToMounters = () =>
new Map([...mounters].map(([appId, { mounter }]) => [appId, mounter]));
const setAppLeaveHandlerMock = () => undefined;
const noop = () => undefined;

const mountersToAppStatus$ = () => {
return new BehaviorSubject(
Expand Down Expand Up @@ -86,7 +86,8 @@ describe('AppContainer', () => {
history={globalHistory}
mounters={mockMountersToMounters()}
appStatuses$={appStatuses$}
setAppLeaveHandler={setAppLeaveHandlerMock}
setAppLeaveHandler={noop}
setIsMounting={noop}
/>
);
});
Expand All @@ -98,7 +99,7 @@ describe('AppContainer', () => {

expect(app1.mounter.mount).toHaveBeenCalled();
expect(dom?.html()).toMatchInlineSnapshot(`
"<div><div>
"<div class=\\"appContainer__loading\\"><span class=\\"euiLoadingSpinner euiLoadingSpinner--large\\"></span></div><div><div>
basename: /app/app1
html: <span>App 1</span>
</div></div>"
Expand All @@ -110,7 +111,7 @@ describe('AppContainer', () => {
expect(app1Unmount).toHaveBeenCalled();
expect(app2.mounter.mount).toHaveBeenCalled();
expect(dom?.html()).toMatchInlineSnapshot(`
"<div><div>
"<div class=\\"appContainer__loading\\"><span class=\\"euiLoadingSpinner euiLoadingSpinner--large\\"></span></div><div><div>
basename: /app/app2
html: <div>App 2</div>
</div></div>"
Expand All @@ -124,7 +125,7 @@ describe('AppContainer', () => {

expect(standardApp.mounter.mount).toHaveBeenCalled();
expect(dom?.html()).toMatchInlineSnapshot(`
"<div><div>
"<div class=\\"appContainer__loading\\"><span class=\\"euiLoadingSpinner euiLoadingSpinner--large\\"></span></div><div><div>
basename: /app/app1
html: <span>App 1</span>
</div></div>"
Expand All @@ -136,7 +137,7 @@ describe('AppContainer', () => {
expect(standardAppUnmount).toHaveBeenCalled();
expect(chromelessApp.mounter.mount).toHaveBeenCalled();
expect(dom?.html()).toMatchInlineSnapshot(`
"<div><div>
"<div class=\\"appContainer__loading\\"><span class=\\"euiLoadingSpinner euiLoadingSpinner--large\\"></span></div><div><div>
basename: /chromeless-a/path
html: <div>Chromeless A</div>
</div></div>"
Expand All @@ -148,7 +149,7 @@ describe('AppContainer', () => {
expect(chromelessAppUnmount).toHaveBeenCalled();
expect(standardApp.mounter.mount).toHaveBeenCalledTimes(2);
expect(dom?.html()).toMatchInlineSnapshot(`
"<div><div>
"<div class=\\"appContainer__loading\\"><span class=\\"euiLoadingSpinner euiLoadingSpinner--large\\"></span></div><div><div>
basename: /app/app1
html: <span>App 1</span>
</div></div>"
Expand All @@ -162,7 +163,7 @@ describe('AppContainer', () => {

expect(chromelessAppA.mounter.mount).toHaveBeenCalled();
expect(dom?.html()).toMatchInlineSnapshot(`
"<div><div>
"<div class=\\"appContainer__loading\\"><span class=\\"euiLoadingSpinner euiLoadingSpinner--large\\"></span></div><div><div>
basename: /chromeless-a/path
html: <div>Chromeless A</div>
</div></div>"
Expand All @@ -174,7 +175,7 @@ describe('AppContainer', () => {
expect(chromelessAppAUnmount).toHaveBeenCalled();
expect(chromelessAppB.mounter.mount).toHaveBeenCalled();
expect(dom?.html()).toMatchInlineSnapshot(`
"<div><div>
"<div class=\\"appContainer__loading\\"><span class=\\"euiLoadingSpinner euiLoadingSpinner--large\\"></span></div><div><div>
basename: /chromeless-b/path
html: <div>Chromeless B</div>
</div></div>"
Expand All @@ -186,7 +187,7 @@ describe('AppContainer', () => {
expect(chromelessAppBUnmount).toHaveBeenCalled();
expect(chromelessAppA.mounter.mount).toHaveBeenCalledTimes(2);
expect(dom?.html()).toMatchInlineSnapshot(`
"<div><div>
"<div class=\\"appContainer__loading\\"><span class=\\"euiLoadingSpinner euiLoadingSpinner--large\\"></span></div><div><div>
basename: /chromeless-a/path
html: <div>Chromeless A</div>
</div></div>"
Expand Down Expand Up @@ -214,7 +215,8 @@ describe('AppContainer', () => {
history={globalHistory}
mounters={mockMountersToMounters()}
appStatuses$={mountersToAppStatus$()}
setAppLeaveHandler={setAppLeaveHandlerMock}
setAppLeaveHandler={noop}
setIsMounting={noop}
/>
);

Expand Down Expand Up @@ -245,7 +247,8 @@ describe('AppContainer', () => {
history={globalHistory}
mounters={mockMountersToMounters()}
appStatuses$={mountersToAppStatus$()}
setAppLeaveHandler={setAppLeaveHandlerMock}
setAppLeaveHandler={noop}
setIsMounting={noop}
/>
);

Expand Down Expand Up @@ -286,7 +289,8 @@ describe('AppContainer', () => {
history={globalHistory}
mounters={mockMountersToMounters()}
appStatuses$={mountersToAppStatus$()}
setAppLeaveHandler={setAppLeaveHandlerMock}
setAppLeaveHandler={noop}
setIsMounting={noop}
/>
);

Expand Down
5 changes: 4 additions & 1 deletion src/core/public/application/integration_tests/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/

import React, { ReactElement } from 'react';
import { act } from 'react-dom/test-utils';
import { mount } from 'enzyme';

import { I18nProvider } from '@kbn/i18n/react';
Expand All @@ -34,7 +35,9 @@ export const createRenderer = (element: ReactElement | null): Renderer => {
return () =>
new Promise(async resolve => {
if (dom) {
dom.update();
await act(async () => {
dom.update();
});
}
setImmediate(() => resolve(dom)); // flushes any pending promises
});
Expand Down
25 changes: 25 additions & 0 deletions src/core/public/application/ui/app_container.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.appContainer__loading {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: $euiZLevel1;
animation-name: appContainerFadeIn;
animation-iteration-count: 1;
animation-timing-function: ease-in;
animation-duration: 2s;
}

@keyframes appContainerFadeIn {
0% {
opacity: 0;
}

50% {
opacity: 0;
}

100% {
opacity: 1;
}
}
Loading

0 comments on commit 8a1d4d0

Please sign in to comment.