From 1bd6b3bbdd59471742af1b4a47a930e74a629c9d Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 22 Oct 2019 14:46:53 +0200 Subject: [PATCH 1/3] local application service --- .../core_plugins/kibana/public/kibana.js | 3 + .../public/local_application_service/index.ts | 20 +++ .../local_application_service.ts | 136 ++++++++++++++++++ .../public/legacy_compat/angular_config.tsx | 20 +++ .../ui/public/routes/route_manager.d.ts | 4 +- 5 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 src/legacy/core_plugins/kibana/public/local_application_service/index.ts create mode 100644 src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts diff --git a/src/legacy/core_plugins/kibana/public/kibana.js b/src/legacy/core_plugins/kibana/public/kibana.js index 6c809e84c8c84..af7c9131caf45 100644 --- a/src/legacy/core_plugins/kibana/public/kibana.js +++ b/src/legacy/core_plugins/kibana/public/kibana.js @@ -58,6 +58,9 @@ import 'ui/agg_response'; import 'ui/agg_types'; import { showAppRedirectNotification } from 'ui/notify'; import 'leaflet'; +import { localApplicationService } from './local_application_service'; + +localApplicationService.apply(routes); routes.enable(); diff --git a/src/legacy/core_plugins/kibana/public/local_application_service/index.ts b/src/legacy/core_plugins/kibana/public/local_application_service/index.ts new file mode 100644 index 0000000000000..2128355ca906a --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/local_application_service/index.ts @@ -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 * from './local_application_service'; diff --git a/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts b/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts new file mode 100644 index 0000000000000..ba7e3921d3537 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts @@ -0,0 +1,136 @@ +/* + * 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 { App } from 'kibana/public'; +import { UIRoutes } from 'ui/routes'; +import { IScope } from 'angular'; +import { npStart } from 'ui/new_platform'; +import { htmlIdGenerator } from '@elastic/eui'; + +interface ForwardDefinition { + legacyAppId: string; + newAppId: string; + keepPrefix: boolean; +} + +const matchAllWithPrefix = (prefixOrApp: string | App) => + `/${typeof prefixOrApp === 'string' ? prefixOrApp : prefixOrApp.id}:tail*?`; + +/** + * To be able to migrate and shim parts of the Kibana app plugin + * while still running some parts of it in the legacy world, this + * service emulates the core application service while using the global + * angular router to switch between apps without page reload. + * + * The id of the apps is used as prefix of the route - when switching between + * to apps, the current application is unmounted. + * + * This service becomes unnecessary once the platform provides a central + * router that handles switching between applications without page reload. + */ +export class LocalApplicationService { + private apps: App[] = []; + private forwards: ForwardDefinition[] = []; + private idGenerator = htmlIdGenerator('kibanaAppLocalApp'); + + /** + * Register an app to be managed by the application service. + * This method works exactly as `core.application.register`. + * + * When an app is mounted, it is responsible for routing. The app + * won't be mounted again if the route changes within the prefix + * of the app (its id). It is fine to use whatever means for handling + * routing within the app. + * + * When switching to a URL outside of the current prefix, the app router + * shouldn't do anything because it doesn't own the routing anymore - + * the local application service takes over routing again, + * unmounts the current app and mounts the next app. + * + * @param app The app descriptor + */ + register(app: App) { + this.apps.push(app); + } + + /** + * Forwards every URL starting with `legacyAppId` to the same URL starting + * with `newAppId` - e.g. `/legacy/my/legacy/path?q=123` gets forwarded to + * `/newApp/my/legacy/path?q=123`. + * + * When setting the `keepPrefix` option, the new app id is simply prepended. + * The example above would become `/newApp/legacy/my/legacy/path?q=123`. + * + * This method can be used to provide backwards compatibility for URLs when + * renaming or nesting plugins. For route changes after the prefix, please + * use the routing mechanism of your app. + * + * @param legacyAppId The name of the old app to forward URLs from + * @param newAppId The name of the new app that handles the URLs now + * @param options Whether the prefix of the old app is kept to nest the legacy + * path into the new path + */ + forwardApp( + legacyAppId: string, + newAppId: string, + options: { keepPrefix: boolean } = { keepPrefix: false } + ) { + this.forwards.push({ legacyAppId, newAppId, ...options }); + } + + /** + * Wires up listeners to handle mounting and unmounting of apps to + * the legacy angular route manager. Once all apps within the Kibana + * plugin are using the local route manager, this implementation can + * be switched to a more lightweight implementation. + * + * @param angularRouteManager The current `ui/routes` instance + */ + apply(angularRouteManager: UIRoutes) { + this.apps.forEach(app => { + const wrapperElementId = this.idGenerator(); + angularRouteManager.when(matchAllWithPrefix(app), { + outerAngularWrapperRoute: true, + reloadOnSearch: false, + reloadOnUrl: false, + template: `
`, + controller($scope: IScope) { + const element = document.getElementById(wrapperElementId)!; + (async () => { + const onUnmount = await app.mount({ core: npStart.core }, { element, appBasePath: '' }); + $scope.$on('$destroy', () => { + onUnmount(); + }); + })(); + }, + }); + }); + + this.forwards.forEach(({ legacyAppId, newAppId, keepPrefix }) => { + angularRouteManager.when(matchAllWithPrefix(legacyAppId), { + redirectTo: (_params: unknown, path: string, search: string) => { + const newPath = `/${newAppId}${keepPrefix ? path : path.replace(legacyAppId, '')}`; + return `${newPath}?${search}`; + }, + }); + }); + } +} + +export const localApplicationService = new LocalApplicationService(); diff --git a/src/legacy/ui/public/legacy_compat/angular_config.tsx b/src/legacy/ui/public/legacy_compat/angular_config.tsx index 8eac31e24530c..6b69e8e5f14b3 100644 --- a/src/legacy/ui/public/legacy_compat/angular_config.tsx +++ b/src/legacy/ui/public/legacy_compat/angular_config.tsx @@ -48,6 +48,12 @@ import { isSystemApiRequest } from '../system_api'; const URL_LIMIT_WARN_WITHIN = 1000; +function isDummyWrapperRoute($route: any) { + return ( + $route.current && $route.current.$$route && $route.current.$$route.outerAngularWrapperRoute + ); +} + export const configureAppAngularModule = (angularModule: IModule) => { const newPlatform = npStart.core; const legacyMetadata = newPlatform.injectedMetadata.getLegacyMetadata(); @@ -187,6 +193,9 @@ const $setupBreadcrumbsAutoClear = (newPlatform: CoreStart) => ( }); $rootScope.$on('$routeChangeSuccess', () => { + if (isDummyWrapperRoute($route)) { + return; + } const current = $route.current || {}; if (breadcrumbSetSinceRouteChange || (current.$$route && current.$$route.redirectTo)) { @@ -226,6 +235,9 @@ const $setupBadgeAutoClear = (newPlatform: CoreStart) => ( }); $rootScope.$on('$routeChangeSuccess', () => { + if (isDummyWrapperRoute($route)) { + return; + } const current = $route.current || {}; if (badgeSetSinceRouteChange || (current.$$route && current.$$route.redirectTo)) { @@ -270,6 +282,9 @@ const $setupHelpExtensionAutoClear = (newPlatform: CoreStart) => ( const $route = $injector.has('$route') ? $injector.get('$route') : {}; $rootScope.$on('$routeChangeStart', () => { + if (isDummyWrapperRoute($route)) { + return; + } helpExtensionSetSinceRouteChange = false; }); @@ -287,11 +302,16 @@ const $setupHelpExtensionAutoClear = (newPlatform: CoreStart) => ( const $setupUrlOverflowHandling = (newPlatform: CoreStart) => ( $location: ILocationService, $rootScope: IRootScopeService, + $injector: any, Private: any, config: any ) => { + const $route = $injector.has('$route') ? $injector.get('$route') : {}; const urlOverflow = new UrlOverflowService(); const check = () => { + if (isDummyWrapperRoute($route)) { + return; + } // disable long url checks when storing state in session storage if (config.get('state:storeInSessionStorage')) { return; diff --git a/src/legacy/ui/public/routes/route_manager.d.ts b/src/legacy/ui/public/routes/route_manager.d.ts index 3471d7e954862..3d1ba88918f55 100644 --- a/src/legacy/ui/public/routes/route_manager.d.ts +++ b/src/legacy/ui/public/routes/route_manager.d.ts @@ -25,8 +25,10 @@ import { ChromeBreadcrumb } from '../../../../core/public'; interface RouteConfiguration { controller?: string | ((...args: any[]) => void); - redirectTo?: string; + redirectTo?: string | ((params: object, path: string, search: string) => string); reloadOnSearch?: boolean; + reloadOnUrl?: boolean; + outerAngularWrapperRoute?: boolean; resolve?: object; template?: string; k7Breadcrumbs?: (...args: any[]) => ChromeBreadcrumb[]; From 5195ebf3052357f7f450cfd7e63de87eec04c6b4 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 28 Oct 2019 19:38:42 +0100 Subject: [PATCH 2/3] review fixes --- .../core_plugins/kibana/public/kibana.js | 4 ++- .../local_application_service.ts | 29 ++++++++++++------- .../public/legacy_compat/angular_config.tsx | 3 +- .../ui/public/routes/route_manager.d.ts | 3 +- 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/legacy/core_plugins/kibana/public/kibana.js b/src/legacy/core_plugins/kibana/public/kibana.js index af7c9131caf45..515c0996a49bc 100644 --- a/src/legacy/core_plugins/kibana/public/kibana.js +++ b/src/legacy/core_plugins/kibana/public/kibana.js @@ -60,7 +60,9 @@ import { showAppRedirectNotification } from 'ui/notify'; import 'leaflet'; import { localApplicationService } from './local_application_service'; -localApplicationService.apply(routes); +localApplicationService.forwardApp('foo', 'discover'); + +localApplicationService.attachToAngular(routes); routes.enable(); diff --git a/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts b/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts index ba7e3921d3537..201fbb28429fc 100644 --- a/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts +++ b/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts @@ -17,7 +17,7 @@ * under the License. */ -import { App } from 'kibana/public'; +import { App, AppUnmount } from 'kibana/public'; import { UIRoutes } from 'ui/routes'; import { IScope } from 'angular'; import { npStart } from 'ui/new_platform'; @@ -30,7 +30,7 @@ interface ForwardDefinition { } const matchAllWithPrefix = (prefixOrApp: string | App) => - `/${typeof prefixOrApp === 'string' ? prefixOrApp : prefixOrApp.id}:tail*?`; + `/${typeof prefixOrApp === 'string' ? prefixOrApp : prefixOrApp.id}/:tail*?`; /** * To be able to migrate and shim parts of the Kibana app plugin @@ -102,7 +102,7 @@ export class LocalApplicationService { * * @param angularRouteManager The current `ui/routes` instance */ - apply(angularRouteManager: UIRoutes) { + attachToAngular(angularRouteManager: UIRoutes) { this.apps.forEach(app => { const wrapperElementId = this.idGenerator(); angularRouteManager.when(matchAllWithPrefix(app), { @@ -112,11 +112,20 @@ export class LocalApplicationService { template: `
`, controller($scope: IScope) { const element = document.getElementById(wrapperElementId)!; + let unmountHandler: AppUnmount | null = null; + let isUnmounted = false; + $scope.$on('$destroy', () => { + if (unmountHandler) { + unmountHandler(); + } + isUnmounted = true; + }); (async () => { - const onUnmount = await app.mount({ core: npStart.core }, { element, appBasePath: '' }); - $scope.$on('$destroy', () => { - onUnmount(); - }); + unmountHandler = await app.mount({ core: npStart.core }, { element, appBasePath: '' }); + // immediately unmount app if scope got destroyed in the meantime + if (isUnmounted) { + unmountHandler(); + } })(); }, }); @@ -124,9 +133,9 @@ export class LocalApplicationService { this.forwards.forEach(({ legacyAppId, newAppId, keepPrefix }) => { angularRouteManager.when(matchAllWithPrefix(legacyAppId), { - redirectTo: (_params: unknown, path: string, search: string) => { - const newPath = `/${newAppId}${keepPrefix ? path : path.replace(legacyAppId, '')}`; - return `${newPath}?${search}`; + resolveRedirectTo: ($location: any) => { + const url = $location.url(); + return `/${newAppId}${keepPrefix ? url : url.replace(legacyAppId, '')}`; }, }); }); diff --git a/src/legacy/ui/public/legacy_compat/angular_config.tsx b/src/legacy/ui/public/legacy_compat/angular_config.tsx index 337144cf88f09..27484fb88f22e 100644 --- a/src/legacy/ui/public/legacy_compat/angular_config.tsx +++ b/src/legacy/ui/public/legacy_compat/angular_config.tsx @@ -18,6 +18,7 @@ */ import { + auto, ICompileProvider, IHttpProvider, IHttpService, @@ -302,7 +303,7 @@ const $setupHelpExtensionAutoClear = (newPlatform: CoreStart) => ( const $setupUrlOverflowHandling = (newPlatform: CoreStart) => ( $location: ILocationService, $rootScope: IRootScopeService, - $injector: any + $injector: auto.IInjectorService ) => { const $route = $injector.has('$route') ? $injector.get('$route') : {}; const urlOverflow = new UrlOverflowService(); diff --git a/src/legacy/ui/public/routes/route_manager.d.ts b/src/legacy/ui/public/routes/route_manager.d.ts index 3d1ba88918f55..56203354f3c20 100644 --- a/src/legacy/ui/public/routes/route_manager.d.ts +++ b/src/legacy/ui/public/routes/route_manager.d.ts @@ -25,7 +25,8 @@ import { ChromeBreadcrumb } from '../../../../core/public'; interface RouteConfiguration { controller?: string | ((...args: any[]) => void); - redirectTo?: string | ((params: object, path: string, search: string) => string); + redirectTo?: string; + resolveRedirectTo?: (...args: any[]) => void; reloadOnSearch?: boolean; reloadOnUrl?: boolean; outerAngularWrapperRoute?: boolean; From a724d7b7b4695f5b4d4cc03429bdfeca25646627 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 30 Oct 2019 08:36:28 +0100 Subject: [PATCH 3/3] review fixes --- src/legacy/core_plugins/kibana/public/kibana.js | 2 -- .../local_application_service/local_application_service.ts | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/legacy/core_plugins/kibana/public/kibana.js b/src/legacy/core_plugins/kibana/public/kibana.js index 515c0996a49bc..fe741a357cbfe 100644 --- a/src/legacy/core_plugins/kibana/public/kibana.js +++ b/src/legacy/core_plugins/kibana/public/kibana.js @@ -60,8 +60,6 @@ import { showAppRedirectNotification } from 'ui/notify'; import 'leaflet'; import { localApplicationService } from './local_application_service'; -localApplicationService.forwardApp('foo', 'discover'); - localApplicationService.attachToAngular(routes); routes.enable(); diff --git a/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts b/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts index 201fbb28429fc..9d87e187fd1e1 100644 --- a/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts +++ b/src/legacy/core_plugins/kibana/public/local_application_service/local_application_service.ts @@ -19,7 +19,7 @@ import { App, AppUnmount } from 'kibana/public'; import { UIRoutes } from 'ui/routes'; -import { IScope } from 'angular'; +import { ILocationService, IScope } from 'angular'; import { npStart } from 'ui/new_platform'; import { htmlIdGenerator } from '@elastic/eui'; @@ -133,7 +133,7 @@ export class LocalApplicationService { this.forwards.forEach(({ legacyAppId, newAppId, keepPrefix }) => { angularRouteManager.when(matchAllWithPrefix(legacyAppId), { - resolveRedirectTo: ($location: any) => { + resolveRedirectTo: ($location: ILocationService) => { const url = $location.url(); return `/${newAppId}${keepPrefix ? url : url.replace(legacyAppId, '')}`; },