diff --git a/src/core/public/application/__snapshots__/application_service.test.ts.snap b/src/core/public/application/__snapshots__/application_service.test.ts.snap new file mode 100644 index 00000000000000..376b320b64ea9a --- /dev/null +++ b/src/core/public/application/__snapshots__/application_service.test.ts.snap @@ -0,0 +1,84 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`#start() getComponent returns renderable JSX tree 1`] = ` + +`; diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts index 4672a42c9eb060..54489fbd182b47 100644 --- a/src/core/public/application/application_service.test.ts +++ b/src/core/public/application/application_service.test.ts @@ -525,17 +525,7 @@ describe('#start()', () => { const { getComponent } = await service.start(startDeps); expect(() => shallow(createElement(getComponent))).not.toThrow(); - expect(getComponent()).toMatchInlineSnapshot(` - - `); + expect(getComponent()).toMatchSnapshot(); }); it('renders null when in legacy mode', async () => { diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index c69b96274aa95e..4d714c8f9dad2d 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs'; -import { map, takeUntil } from 'rxjs/operators'; +import { map, shareReplay, takeUntil } from 'rxjs/operators'; import { createBrowserHistory, History } from 'history'; import { InjectedMetadataSetup } from '../injected_metadata'; @@ -256,6 +256,11 @@ export class ApplicationService { ) .subscribe(apps => applications$.next(apps)); + const applicationStatuses$ = applications$.pipe( + map(apps => new Map([...apps.entries()].map(([id, app]) => [id, app.status!]))), + shareReplay(1) + ); + return { applications$, capabilities, @@ -264,11 +269,6 @@ export class ApplicationService { getUrlForApp: (appId, { path }: { path?: string } = {}) => getAppUrl(availableMounters, appId, path), navigateToApp: async (appId, { path, state }: { path?: string; state?: any } = {}) => { - const app = applications$.value.get(appId); - if (app && app.status !== AppStatus.accessible) { - // should probably redirect to the error page instead - throw new Error(`Trying to navigate to an inaccessible application: ${appId}`); - } if (await this.shouldNavigate(overlays)) { this.appLeaveHandlers.delete(this.currentAppId$.value!); this.navigate!(getAppUrl(availableMounters, appId, path), state); @@ -283,6 +283,7 @@ export class ApplicationService { ); diff --git a/src/core/public/application/integration_tests/router.test.tsx b/src/core/public/application/integration_tests/router.test.tsx index cc71cf8722df4d..4d83ab67810afa 100644 --- a/src/core/public/application/integration_tests/router.test.tsx +++ b/src/core/public/application/integration_tests/router.test.tsx @@ -18,15 +18,18 @@ */ import React from 'react'; +import { BehaviorSubject } from 'rxjs'; import { createMemoryHistory, History, createHashHistory } from 'history'; import { AppRouter, AppNotFound } from '../ui'; import { EitherApp, MockedMounterMap, MockedMounterTuple } from '../test_types'; import { createRenderer, createAppMounter, createLegacyAppMounter } from './utils'; +import { AppStatus } from '../types'; describe('AppContainer', () => { let mounters: MockedMounterMap; let history: History; + let appStatuses$: BehaviorSubject>; let update: ReturnType; const navigate = (path: string) => { @@ -38,6 +41,17 @@ describe('AppContainer', () => { new Map([...mounters].map(([appId, { mounter }]) => [appId, mounter])); const setAppLeaveHandlerMock = () => undefined; + const mountersToAppStatus$ = () => { + return new BehaviorSubject( + new Map( + [...mounters.keys()].map(id => [ + id, + id.startsWith('disabled') ? AppStatus.inaccessible : AppStatus.accessible, + ]) + ) + ); + }; + beforeEach(() => { mounters = new Map([ createAppMounter('app1', 'App 1'), @@ -45,12 +59,16 @@ describe('AppContainer', () => { createAppMounter('app2', '
App 2
'), createLegacyAppMounter('baseApp:legacyApp2', jest.fn()), createAppMounter('app3', '
App 3
', '/custom/path'), + createAppMounter('disabledApp', '
Disabled app
'), + createLegacyAppMounter('disabledLegacyApp', jest.fn()), ] as Array>); history = createMemoryHistory(); + appStatuses$ = mountersToAppStatus$(); update = createRenderer( ); @@ -89,6 +107,7 @@ describe('AppContainer', () => { ); @@ -107,6 +126,7 @@ describe('AppContainer', () => { ); @@ -147,6 +167,7 @@ describe('AppContainer', () => { ); @@ -202,4 +223,16 @@ describe('AppContainer', () => { expect(dom?.exists(AppNotFound)).toBe(true); }); + + it('displays error page if app is inaccessible', async () => { + const dom = await navigate('/app/disabledApp'); + + expect(dom?.exists(AppNotFound)).toBe(true); + }); + + it('displays error page if legacy app is inaccessible', async () => { + const dom = await navigate('/app/disabledLegacyApp'); + + expect(dom?.exists(AppNotFound)).toBe(true); + }); }); diff --git a/src/core/public/application/ui/app_container.tsx b/src/core/public/application/ui/app_container.tsx index 8afd4d0ca05514..6a630608b2c205 100644 --- a/src/core/public/application/ui/app_container.tsx +++ b/src/core/public/application/ui/app_container.tsx @@ -26,12 +26,13 @@ import React, { MutableRefObject, } from 'react'; -import { AppUnmount, Mounter, AppLeaveHandler } from '../types'; +import { AppLeaveHandler, AppStatus, AppUnmount, Mounter } from '../types'; import { AppNotFound } from './app_not_found_screen'; interface Props { appId: string; mounter?: Mounter; + appStatus: AppStatus; setAppLeaveHandler: (appId: string, handler: AppLeaveHandler) => void; } @@ -39,10 +40,12 @@ export const AppContainer: FunctionComponent = ({ mounter, appId, setAppLeaveHandler, + appStatus, }: Props) => { const [appNotFound, setAppNotFound] = useState(false); const elementRef = useRef(null); const unmountRef: MutableRefObject = useRef(null); + // const appStatus = useObservable(appStatus$); useLayoutEffect(() => { const unmount = () => { @@ -52,7 +55,7 @@ export const AppContainer: FunctionComponent = ({ } }; const mount = async () => { - if (!mounter) { + if (!mounter || appStatus !== AppStatus.accessible) { return setAppNotFound(true); } @@ -71,7 +74,7 @@ export const AppContainer: FunctionComponent = ({ mount(); return unmount; - }, [appId, mounter, setAppLeaveHandler]); + }, [appId, appStatus, mounter, setAppLeaveHandler]); return ( diff --git a/src/core/public/application/ui/app_not_found_screen.tsx b/src/core/public/application/ui/app_not_found_screen.tsx index 73a999c5dbf162..0d651ee0480960 100644 --- a/src/core/public/application/ui/app_not_found_screen.tsx +++ b/src/core/public/application/ui/app_not_found_screen.tsx @@ -22,7 +22,7 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; export const AppNotFound = () => ( - + ; history: History; + appStatuses$: Observable>; setAppLeaveHandler: (appId: string, handler: AppLeaveHandler) => void; } @@ -34,45 +37,59 @@ interface Params { appId: string; } -export const AppRouter: FunctionComponent = ({ history, mounters, setAppLeaveHandler }) => ( - - - {[...mounters].flatMap(([appId, mounter]) => - // Remove /app paths from the routes as they will be handled by the - // "named" route parameter `:appId` below - mounter.appBasePath.startsWith('/app') - ? [] - : [ - ( - - )} - />, - ] - )} - ) => { - // Find the mounter including legacy mounters with subapps: - const [id, mounter] = mounters.has(appId) - ? [appId, mounters.get(appId)] - : [...mounters].filter(([key]) => key.split(':')[0] === appId)[0] ?? []; +export const AppRouter: FunctionComponent = ({ + history, + mounters, + setAppLeaveHandler, + appStatuses$, +}) => { + const appStatuses = useObservable(appStatuses$, new Map()); + return ( + + + {[...mounters].flatMap(([appId, mounter]) => + // Remove /app paths from the routes as they will be handled by the + // "named" route parameter `:appId` below + mounter.appBasePath.startsWith('/app') + ? [] + : [ + ( + + )} + />, + ] + )} + ) => { + // Find the mounter including legacy mounters with subapps: + const [id, mounter] = mounters.has(appId) + ? [appId, mounters.get(appId)] + : [...mounters].filter(([key]) => key.split(':')[0] === appId)[0] ?? []; - return ( - - ); - }} - /> - - -); + return ( + + ); + }} + /> + + + ); +}; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html index 4b3014fd28a51c..625227be3c2d23 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html @@ -83,8 +83,8 @@