From bc2e63f928d7c27610400adc964710399e04c014 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Thu, 25 Jun 2020 13:51:21 +0200 Subject: [PATCH] add the `exactRoute` property to app registration (#69772) (#69897) * add the `exactRoute` property * add missing doc file * nits on jsdoc --- ...ibana-plugin-core-public.app.exactroute.md | 30 ++++++ .../public/kibana-plugin-core-public.app.md | 1 + .../application/application_service.tsx | 2 + .../integration_tests/router.test.tsx | 100 +++++++++++------- .../application/integration_tests/utils.tsx | 4 + src/core/public/application/types.ts | 19 ++++ .../application/ui/app_container.test.tsx | 2 + src/core/public/application/ui/app_router.tsx | 1 + src/core/public/public.api.md | 1 + 9 files changed, 122 insertions(+), 38 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.app.exactroute.md diff --git a/docs/development/core/public/kibana-plugin-core-public.app.exactroute.md b/docs/development/core/public/kibana-plugin-core-public.app.exactroute.md new file mode 100644 index 0000000000000..d1e0be17a92b2 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.app.exactroute.md @@ -0,0 +1,30 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [exactRoute](./kibana-plugin-core-public.app.exactroute.md) + +## App.exactRoute property + +If set to true, the application's route will only be checked against an exact match. Defaults to `false`. + +Signature: + +```typescript +exactRoute?: boolean; +``` + +## Example + + +```ts +core.application.register({ + id: 'my_app', + title: 'My App' + exactRoute: true, + mount: () => { ... }, +}) + +// '[basePath]/app/my_app' will be matched +// '[basePath]/app/my_app/some/path' will not be matched + +``` + diff --git a/docs/development/core/public/kibana-plugin-core-public.app.md b/docs/development/core/public/kibana-plugin-core-public.app.md index 90737d241f548..8dd60972549f9 100644 --- a/docs/development/core/public/kibana-plugin-core-public.app.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.md @@ -18,5 +18,6 @@ export interface App extends AppBase | --- | --- | --- | | [appRoute](./kibana-plugin-core-public.app.approute.md) | string | Override the application's routing path from /app/${id}. Must be unique across registered applications. Should not include the base path from HTTP. | | [chromeless](./kibana-plugin-core-public.app.chromeless.md) | boolean | Hide the UI chrome when the application is mounted. Defaults to false. Takes precedence over chrome service visibility settings. | +| [exactRoute](./kibana-plugin-core-public.app.exactroute.md) | boolean | If set to true, the application's route will only be checked against an exact match. Defaults to false. | | [mount](./kibana-plugin-core-public.app.mount.md) | AppMount<HistoryLocationState> | AppMountDeprecated<HistoryLocationState> | A mount function called when the user navigates to this app's route. May have signature of [AppMount](./kibana-plugin-core-public.appmount.md) or [AppMountDeprecated](./kibana-plugin-core-public.appmountdeprecated.md). | diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index 95361d8287c71..d7f15decb255d 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -201,6 +201,7 @@ export class ApplicationService { this.mounters.set(app.id, { appRoute: app.appRoute!, appBasePath: basePath.prepend(app.appRoute!), + exactRoute: app.exactRoute ?? false, mount: wrapMount(plugin, app), unmountBeforeMounting: false, legacy: false, @@ -236,6 +237,7 @@ export class ApplicationService { this.mounters.set(app.id, { appRoute, appBasePath, + exactRoute: false, mount, unmountBeforeMounting: true, legacy: true, diff --git a/src/core/public/application/integration_tests/router.test.tsx b/src/core/public/application/integration_tests/router.test.tsx index 2827b93f6d17e..f992e121437a9 100644 --- a/src/core/public/application/integration_tests/router.test.tsx +++ b/src/core/public/application/integration_tests/router.test.tsx @@ -30,7 +30,6 @@ import { ScopedHistory } from '../scoped_history'; describe('AppRouter', () => { let mounters: MockedMounterMap; let globalHistory: History; - let appStatuses$: BehaviorSubject>; let update: ReturnType; let scopedAppHistory: History; @@ -53,6 +52,17 @@ describe('AppRouter', () => { ); }; + const createMountersRenderer = () => + createRenderer( + + ); + beforeEach(() => { mounters = new Map([ createAppMounter({ appId: 'app1', html: 'App 1' }), @@ -90,16 +100,7 @@ describe('AppRouter', () => { }), ] as Array>); globalHistory = createMemoryHistory(); - appStatuses$ = mountersToAppStatus$(); - update = createRenderer( - - ); + update = createMountersRenderer(); }); it('calls mount handler and returned unmount function when navigating between apps', async () => { @@ -220,15 +221,7 @@ describe('AppRouter', () => { }) ); globalHistory = createMemoryHistory(); - update = createRenderer( - - ); + update = createMountersRenderer(); await navigate('/fake-login'); @@ -252,15 +245,7 @@ describe('AppRouter', () => { }) ); globalHistory = createMemoryHistory(); - update = createRenderer( - - ); + update = createMountersRenderer(); await navigate('/spaces/fake-login'); @@ -268,6 +253,53 @@ describe('AppRouter', () => { expect(mounters.get('login')!.mounter.mount).not.toHaveBeenCalled(); }); + it('should mount an exact route app only when the path is an exact match', async () => { + mounters.set( + ...createAppMounter({ + appId: 'exactApp', + html: '
exact app
', + exactRoute: true, + appRoute: '/app/exact-app', + }) + ); + + globalHistory = createMemoryHistory(); + update = createMountersRenderer(); + + await navigate('/app/exact-app/some-path'); + + expect(mounters.get('exactApp')!.mounter.mount).not.toHaveBeenCalled(); + + await navigate('/app/exact-app'); + + expect(mounters.get('exactApp')!.mounter.mount).toHaveBeenCalledTimes(1); + }); + + it('should mount an an app with a route nested in an exact route app', async () => { + mounters.set( + ...createAppMounter({ + appId: 'exactApp', + html: '
exact app
', + exactRoute: true, + appRoute: '/app/exact-app', + }) + ); + mounters.set( + ...createAppMounter({ + appId: 'nestedApp', + html: '
nested app
', + appRoute: '/app/exact-app/another-app', + }) + ); + globalHistory = createMemoryHistory(); + update = createMountersRenderer(); + + await navigate('/app/exact-app/another-app'); + + expect(mounters.get('exactApp')!.mounter.mount).not.toHaveBeenCalled(); + expect(mounters.get('nestedApp')!.mounter.mount).toHaveBeenCalledTimes(1); + }); + it('should not remount when changing pages within app', async () => { const { mounter, unmount } = mounters.get('app1')!; await navigate('/app/app1/page1'); @@ -304,15 +336,7 @@ describe('AppRouter', () => { it('should not remount when when changing pages within app using hash history', async () => { globalHistory = createHashHistory(); - update = createRenderer( - - ); + update = createMountersRenderer(); const { mounter, unmount } = mounters.get('app1')!; await navigate('/app/app1/page1'); diff --git a/src/core/public/application/integration_tests/utils.tsx b/src/core/public/application/integration_tests/utils.tsx index 8590fb3c820ef..80a7fc2c2cad6 100644 --- a/src/core/public/application/integration_tests/utils.tsx +++ b/src/core/public/application/integration_tests/utils.tsx @@ -47,11 +47,13 @@ export const createAppMounter = ({ appId, html = `
App ${appId}
`, appRoute = `/app/${appId}`, + exactRoute = false, extraMountHook, }: { appId: string; html?: string; appRoute?: string; + exactRoute?: boolean; extraMountHook?: (params: AppMountParameters) => void; }): MockedMounterTuple => { const unmount = jest.fn(); @@ -62,6 +64,7 @@ export const createAppMounter = ({ appRoute, appBasePath: appRoute, legacy: false, + exactRoute, mount: jest.fn(async (params: AppMountParameters) => { const { appBasePath: basename, element } = params; Object.assign(element, { @@ -90,6 +93,7 @@ export const createLegacyAppMounter = ( appBasePath: `/app/${appId.split(':')[0]}`, unmountBeforeMounting: true, legacy: true, + exactRoute: false, mount: legacyMount, }, unmount: jest.fn(), diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index 44b095bd9e6d8..6926b6acf2411 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -234,6 +234,24 @@ export interface App extends AppBase { * base path from HTTP. */ appRoute?: string; + + /** + * If set to true, the application's route will only be checked against an exact match. Defaults to `false`. + * + * @example + * ```ts + * core.application.register({ + * id: 'my_app', + * title: 'My App' + * exactRoute: true, + * mount: () => { ... }, + * }) + * + * // '[basePath]/app/my_app' will be matched + * // '[basePath]/app/my_app/some/path' will not be matched + * ``` + */ + exactRoute?: boolean; } /** @public */ @@ -569,6 +587,7 @@ export type Mounter = SelectivePartial< appBasePath: string; mount: T extends LegacyApp ? LegacyAppMounter : AppMounter; legacy: boolean; + exactRoute: boolean; unmountBeforeMounting: T extends LegacyApp ? true : boolean; }, T extends LegacyApp ? never : 'unmountBeforeMounting' diff --git a/src/core/public/application/ui/app_container.test.tsx b/src/core/public/application/ui/app_container.test.tsx index 229354a014103..a94313dd53abb 100644 --- a/src/core/public/application/ui/app_container.test.tsx +++ b/src/core/public/application/ui/app_container.test.tsx @@ -55,6 +55,7 @@ describe('AppContainer', () => { appRoute: '/some-route', unmountBeforeMounting: false, legacy: false, + exactRoute: false, mount: async ({ element }: AppMountParameters) => { await promise; const container = document.createElement('div'); @@ -143,6 +144,7 @@ describe('AppContainer', () => { appRoute: '/some-route', unmountBeforeMounting: false, legacy: false, + exactRoute: false, mount: async ({ element }: AppMountParameters) => { await waitPromise; throw new Error(`Mounting failed!`); diff --git a/src/core/public/application/ui/app_router.tsx b/src/core/public/application/ui/app_router.tsx index 5d02f96134b27..f2d2d1e6587ac 100644 --- a/src/core/public/application/ui/app_router.tsx +++ b/src/core/public/application/ui/app_router.tsx @@ -63,6 +63,7 @@ export const AppRouter: FunctionComponent = ({ ( extends AppBase { appRoute?: string; chromeless?: boolean; + exactRoute?: boolean; mount: AppMount | AppMountDeprecated; }