This commit is contained in:
parent
af89ad78bf
commit
8a1d4d0b32
|
@ -80,5 +80,6 @@ exports[`#start() getComponent returns renderable JSX tree 1`] = `
|
|||
}
|
||||
mounters={Map {}}
|
||||
setAppLeaveHandler={[Function]}
|
||||
setIsMounting={[Function]}
|
||||
/>
|
||||
`;
|
||||
|
|
|
@ -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';
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
@ -303,6 +306,7 @@ export class ApplicationService {
|
|||
mounters={availableMounters}
|
||||
appStatuses$={applicationStatuses$}
|
||||
setAppLeaveHandler={this.setAppLeaveHandler}
|
||||
setIsMounting={isMounting => httpLoadingCount$.next(isMounting ? 1 : 0)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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(
|
||||
|
@ -86,7 +86,8 @@ describe('AppContainer', () => {
|
|||
history={globalHistory}
|
||||
mounters={mockMountersToMounters()}
|
||||
appStatuses$={appStatuses$}
|
||||
setAppLeaveHandler={setAppLeaveHandlerMock}
|
||||
setAppLeaveHandler={noop}
|
||||
setIsMounting={noop}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -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>"
|
||||
|
@ -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>"
|
||||
|
@ -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>"
|
||||
|
@ -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>"
|
||||
|
@ -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>"
|
||||
|
@ -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>"
|
||||
|
@ -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>"
|
||||
|
@ -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>"
|
||||
|
@ -214,7 +215,8 @@ describe('AppContainer', () => {
|
|||
history={globalHistory}
|
||||
mounters={mockMountersToMounters()}
|
||||
appStatuses$={mountersToAppStatus$()}
|
||||
setAppLeaveHandler={setAppLeaveHandlerMock}
|
||||
setAppLeaveHandler={noop}
|
||||
setIsMounting={noop}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -245,7 +247,8 @@ describe('AppContainer', () => {
|
|||
history={globalHistory}
|
||||
mounters={mockMountersToMounters()}
|
||||
appStatuses$={mountersToAppStatus$()}
|
||||
setAppLeaveHandler={setAppLeaveHandlerMock}
|
||||
setAppLeaveHandler={noop}
|
||||
setIsMounting={noop}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -286,7 +289,8 @@ describe('AppContainer', () => {
|
|||
history={globalHistory}
|
||||
mounters={mockMountersToMounters()}
|
||||
appStatuses$={mountersToAppStatus$()}
|
||||
setAppLeaveHandler={setAppLeaveHandlerMock}
|
||||
setAppLeaveHandler={noop}
|
||||
setIsMounting={noop}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
@ -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
|
||||
});
|
||||
|
|
25
src/core/public/application/ui/app_container.scss
Normal file
25
src/core/public/application/ui/app_container.scss
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import { AppContainer } from './app_container';
|
||||
|
@ -28,6 +29,12 @@ import { ScopedHistory } from '../scoped_history';
|
|||
describe('AppContainer', () => {
|
||||
const appId = 'someApp';
|
||||
const setAppLeaveHandler = jest.fn();
|
||||
const setIsMounting = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
setAppLeaveHandler.mockClear();
|
||||
setIsMounting.mockClear();
|
||||
});
|
||||
|
||||
const flushPromises = async () => {
|
||||
await new Promise(async resolve => {
|
||||
|
@ -67,6 +74,7 @@ describe('AppContainer', () => {
|
|||
appStatus={AppStatus.inaccessible}
|
||||
mounter={mounter}
|
||||
setAppLeaveHandler={setAppLeaveHandler}
|
||||
setIsMounting={setIsMounting}
|
||||
createScopedHistory={(appPath: string) =>
|
||||
// Create a history using the appPath as the current location
|
||||
new ScopedHistory(createMemoryHistory({ initialEntries: [appPath] }), appPath)
|
||||
|
@ -86,10 +94,86 @@ describe('AppContainer', () => {
|
|||
|
||||
expect(wrapper.text()).toEqual('');
|
||||
|
||||
resolvePromise();
|
||||
await flushPromises();
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
resolvePromise();
|
||||
await flushPromises();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain('some-content');
|
||||
});
|
||||
|
||||
it('should call setIsMounting while mounting', async () => {
|
||||
const [waitPromise, resolvePromise] = createResolver();
|
||||
const mounter = createMounter(waitPromise);
|
||||
|
||||
const wrapper = mount(
|
||||
<AppContainer
|
||||
appPath={`/app/${appId}`}
|
||||
appId={appId}
|
||||
appStatus={AppStatus.accessible}
|
||||
mounter={mounter}
|
||||
setAppLeaveHandler={setAppLeaveHandler}
|
||||
setIsMounting={setIsMounting}
|
||||
createScopedHistory={(appPath: string) =>
|
||||
// Create a history using the appPath as the current location
|
||||
new ScopedHistory(createMemoryHistory({ initialEntries: [appPath] }), appPath)
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(setIsMounting).toHaveBeenCalledTimes(1);
|
||||
expect(setIsMounting).toHaveBeenLastCalledWith(true);
|
||||
|
||||
await act(async () => {
|
||||
resolvePromise();
|
||||
await flushPromises();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(setIsMounting).toHaveBeenCalledTimes(2);
|
||||
expect(setIsMounting).toHaveBeenLastCalledWith(false);
|
||||
});
|
||||
|
||||
it('should call setIsMounting(false) if mounting throws', async () => {
|
||||
const [waitPromise, resolvePromise] = createResolver();
|
||||
const mounter = {
|
||||
appBasePath: '/base-path',
|
||||
appRoute: '/some-route',
|
||||
unmountBeforeMounting: false,
|
||||
mount: async ({ element }: AppMountParameters) => {
|
||||
await waitPromise;
|
||||
throw new Error(`Mounting failed!`);
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<AppContainer
|
||||
appPath={`/app/${appId}`}
|
||||
appId={appId}
|
||||
appStatus={AppStatus.accessible}
|
||||
mounter={mounter}
|
||||
setAppLeaveHandler={setAppLeaveHandler}
|
||||
setIsMounting={setIsMounting}
|
||||
createScopedHistory={(appPath: string) =>
|
||||
// Create a history using the appPath as the current location
|
||||
new ScopedHistory(createMemoryHistory({ initialEntries: [appPath] }), appPath)
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(setIsMounting).toHaveBeenCalledTimes(1);
|
||||
expect(setIsMounting).toHaveBeenLastCalledWith(true);
|
||||
|
||||
// await expect(
|
||||
await act(async () => {
|
||||
resolvePromise();
|
||||
await flushPromises();
|
||||
wrapper.update();
|
||||
});
|
||||
// ).rejects.toThrow();
|
||||
|
||||
expect(setIsMounting).toHaveBeenCalledTimes(2);
|
||||
expect(setIsMounting).toHaveBeenLastCalledWith(false);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -26,9 +26,11 @@ import React, {
|
|||
MutableRefObject,
|
||||
} from 'react';
|
||||
|
||||
import { EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { AppLeaveHandler, AppStatus, AppUnmount, Mounter } from '../types';
|
||||
import { AppNotFound } from './app_not_found_screen';
|
||||
import { ScopedHistory } from '../scoped_history';
|
||||
import './app_container.scss';
|
||||
|
||||
interface Props {
|
||||
/** Path application is mounted on without the global basePath */
|
||||
|
@ -38,6 +40,7 @@ interface Props {
|
|||
appStatus: AppStatus;
|
||||
setAppLeaveHandler: (appId: string, handler: AppLeaveHandler) => void;
|
||||
createScopedHistory: (appUrl: string) => ScopedHistory;
|
||||
setIsMounting: (isMounting: boolean) => void;
|
||||
}
|
||||
|
||||
export const AppContainer: FunctionComponent<Props> = ({
|
||||
|
@ -47,7 +50,9 @@ export const AppContainer: FunctionComponent<Props> = ({
|
|||
setAppLeaveHandler,
|
||||
createScopedHistory,
|
||||
appStatus,
|
||||
setIsMounting,
|
||||
}: Props) => {
|
||||
const [showSpinner, setShowSpinner] = useState(true);
|
||||
const [appNotFound, setAppNotFound] = useState(false);
|
||||
const elementRef = useRef<HTMLDivElement>(null);
|
||||
const unmountRef: MutableRefObject<AppUnmount | null> = useRef<AppUnmount>(null);
|
||||
|
@ -65,28 +70,42 @@ export const AppContainer: FunctionComponent<Props> = ({
|
|||
}
|
||||
setAppNotFound(false);
|
||||
|
||||
setIsMounting(true);
|
||||
if (mounter.unmountBeforeMounting) {
|
||||
unmount();
|
||||
}
|
||||
|
||||
const mount = async () => {
|
||||
unmountRef.current =
|
||||
(await mounter.mount({
|
||||
appBasePath: mounter.appBasePath,
|
||||
history: createScopedHistory(appPath),
|
||||
element: elementRef.current!,
|
||||
onAppLeave: handler => setAppLeaveHandler(appId, handler),
|
||||
})) || null;
|
||||
setShowSpinner(true);
|
||||
try {
|
||||
unmountRef.current =
|
||||
(await mounter.mount({
|
||||
appBasePath: mounter.appBasePath,
|
||||
history: createScopedHistory(appPath),
|
||||
element: elementRef.current!,
|
||||
onAppLeave: handler => setAppLeaveHandler(appId, handler),
|
||||
})) || null;
|
||||
} catch (e) {
|
||||
// TODO: add error UI
|
||||
} finally {
|
||||
setShowSpinner(false);
|
||||
setIsMounting(false);
|
||||
}
|
||||
};
|
||||
|
||||
mount();
|
||||
|
||||
return unmount;
|
||||
}, [appId, appStatus, mounter, createScopedHistory, setAppLeaveHandler, appPath]);
|
||||
}, [appId, appStatus, mounter, createScopedHistory, setAppLeaveHandler, appPath, setIsMounting]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{appNotFound && <AppNotFound />}
|
||||
{showSpinner && (
|
||||
<div className="appContainer__loading">
|
||||
<EuiLoadingSpinner size="l" />
|
||||
</div>
|
||||
)}
|
||||
<div key={appId} ref={elementRef} />
|
||||
</Fragment>
|
||||
);
|
||||
|
|
|
@ -32,6 +32,7 @@ interface Props {
|
|||
history: History;
|
||||
appStatuses$: Observable<Map<string, AppStatus>>;
|
||||
setAppLeaveHandler: (appId: string, handler: AppLeaveHandler) => void;
|
||||
setIsMounting: (isMounting: boolean) => void;
|
||||
}
|
||||
|
||||
interface Params {
|
||||
|
@ -43,6 +44,7 @@ export const AppRouter: FunctionComponent<Props> = ({
|
|||
mounters,
|
||||
setAppLeaveHandler,
|
||||
appStatuses$,
|
||||
setIsMounting,
|
||||
}) => {
|
||||
const appStatuses = useObservable(appStatuses$, new Map());
|
||||
const createScopedHistory = useMemo(
|
||||
|
@ -67,7 +69,7 @@ export const AppRouter: FunctionComponent<Props> = ({
|
|||
appPath={url}
|
||||
appStatus={appStatuses.get(appId) ?? AppStatus.inaccessible}
|
||||
createScopedHistory={createScopedHistory}
|
||||
{...{ appId, mounter, setAppLeaveHandler }}
|
||||
{...{ appId, mounter, setAppLeaveHandler, setIsMounting }}
|
||||
/>
|
||||
)}
|
||||
/>,
|
||||
|
@ -92,7 +94,7 @@ export const AppRouter: FunctionComponent<Props> = ({
|
|||
appId={id}
|
||||
appStatus={appStatuses.get(id) ?? AppStatus.inaccessible}
|
||||
createScopedHistory={createScopedHistory}
|
||||
{...{ mounter, setAppLeaveHandler }}
|
||||
{...{ mounter, setAppLeaveHandler, setIsMounting }}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
|
Loading…
Reference in a new issue