Skip to content

Commit 1fab577

Browse files
authored
Kibana app migration: Local application service (#48898)
1 parent 29c5b44 commit 1fab577

File tree

5 files changed

+193
-1
lines changed

5 files changed

+193
-1
lines changed

src/legacy/core_plugins/kibana/public/kibana.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ import 'ui/agg_response';
5858
import 'ui/agg_types';
5959
import { showAppRedirectNotification } from 'ui/notify';
6060
import 'leaflet';
61+
import { localApplicationService } from './local_application_service';
62+
63+
localApplicationService.attachToAngular(routes);
6164

6265
routes.enable();
6366

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
export * from './local_application_service';
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import { App, AppUnmount } from 'kibana/public';
21+
import { UIRoutes } from 'ui/routes';
22+
import { ILocationService, IScope } from 'angular';
23+
import { npStart } from 'ui/new_platform';
24+
import { htmlIdGenerator } from '@elastic/eui';
25+
26+
interface ForwardDefinition {
27+
legacyAppId: string;
28+
newAppId: string;
29+
keepPrefix: boolean;
30+
}
31+
32+
const matchAllWithPrefix = (prefixOrApp: string | App) =>
33+
`/${typeof prefixOrApp === 'string' ? prefixOrApp : prefixOrApp.id}/:tail*?`;
34+
35+
/**
36+
* To be able to migrate and shim parts of the Kibana app plugin
37+
* while still running some parts of it in the legacy world, this
38+
* service emulates the core application service while using the global
39+
* angular router to switch between apps without page reload.
40+
*
41+
* The id of the apps is used as prefix of the route - when switching between
42+
* to apps, the current application is unmounted.
43+
*
44+
* This service becomes unnecessary once the platform provides a central
45+
* router that handles switching between applications without page reload.
46+
*/
47+
export class LocalApplicationService {
48+
private apps: App[] = [];
49+
private forwards: ForwardDefinition[] = [];
50+
private idGenerator = htmlIdGenerator('kibanaAppLocalApp');
51+
52+
/**
53+
* Register an app to be managed by the application service.
54+
* This method works exactly as `core.application.register`.
55+
*
56+
* When an app is mounted, it is responsible for routing. The app
57+
* won't be mounted again if the route changes within the prefix
58+
* of the app (its id). It is fine to use whatever means for handling
59+
* routing within the app.
60+
*
61+
* When switching to a URL outside of the current prefix, the app router
62+
* shouldn't do anything because it doesn't own the routing anymore -
63+
* the local application service takes over routing again,
64+
* unmounts the current app and mounts the next app.
65+
*
66+
* @param app The app descriptor
67+
*/
68+
register(app: App) {
69+
this.apps.push(app);
70+
}
71+
72+
/**
73+
* Forwards every URL starting with `legacyAppId` to the same URL starting
74+
* with `newAppId` - e.g. `/legacy/my/legacy/path?q=123` gets forwarded to
75+
* `/newApp/my/legacy/path?q=123`.
76+
*
77+
* When setting the `keepPrefix` option, the new app id is simply prepended.
78+
* The example above would become `/newApp/legacy/my/legacy/path?q=123`.
79+
*
80+
* This method can be used to provide backwards compatibility for URLs when
81+
* renaming or nesting plugins. For route changes after the prefix, please
82+
* use the routing mechanism of your app.
83+
*
84+
* @param legacyAppId The name of the old app to forward URLs from
85+
* @param newAppId The name of the new app that handles the URLs now
86+
* @param options Whether the prefix of the old app is kept to nest the legacy
87+
* path into the new path
88+
*/
89+
forwardApp(
90+
legacyAppId: string,
91+
newAppId: string,
92+
options: { keepPrefix: boolean } = { keepPrefix: false }
93+
) {
94+
this.forwards.push({ legacyAppId, newAppId, ...options });
95+
}
96+
97+
/**
98+
* Wires up listeners to handle mounting and unmounting of apps to
99+
* the legacy angular route manager. Once all apps within the Kibana
100+
* plugin are using the local route manager, this implementation can
101+
* be switched to a more lightweight implementation.
102+
*
103+
* @param angularRouteManager The current `ui/routes` instance
104+
*/
105+
attachToAngular(angularRouteManager: UIRoutes) {
106+
this.apps.forEach(app => {
107+
const wrapperElementId = this.idGenerator();
108+
angularRouteManager.when(matchAllWithPrefix(app), {
109+
outerAngularWrapperRoute: true,
110+
reloadOnSearch: false,
111+
reloadOnUrl: false,
112+
template: `<div style="height:100%" id="${wrapperElementId}"></div>`,
113+
controller($scope: IScope) {
114+
const element = document.getElementById(wrapperElementId)!;
115+
let unmountHandler: AppUnmount | null = null;
116+
let isUnmounted = false;
117+
$scope.$on('$destroy', () => {
118+
if (unmountHandler) {
119+
unmountHandler();
120+
}
121+
isUnmounted = true;
122+
});
123+
(async () => {
124+
unmountHandler = await app.mount({ core: npStart.core }, { element, appBasePath: '' });
125+
// immediately unmount app if scope got destroyed in the meantime
126+
if (isUnmounted) {
127+
unmountHandler();
128+
}
129+
})();
130+
},
131+
});
132+
});
133+
134+
this.forwards.forEach(({ legacyAppId, newAppId, keepPrefix }) => {
135+
angularRouteManager.when(matchAllWithPrefix(legacyAppId), {
136+
resolveRedirectTo: ($location: ILocationService) => {
137+
const url = $location.url();
138+
return `/${newAppId}${keepPrefix ? url : url.replace(legacyAppId, '')}`;
139+
},
140+
});
141+
});
142+
}
143+
}
144+
145+
export const localApplicationService = new LocalApplicationService();

src/legacy/ui/public/legacy_compat/angular_config.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
*/
1919

2020
import {
21+
auto,
2122
ICompileProvider,
2223
IHttpProvider,
2324
IHttpService,
@@ -48,6 +49,12 @@ import { isSystemApiRequest } from '../system_api';
4849

4950
const URL_LIMIT_WARN_WITHIN = 1000;
5051

52+
function isDummyWrapperRoute($route: any) {
53+
return (
54+
$route.current && $route.current.$$route && $route.current.$$route.outerAngularWrapperRoute
55+
);
56+
}
57+
5158
export const configureAppAngularModule = (angularModule: IModule) => {
5259
const newPlatform = npStart.core;
5360
const legacyMetadata = newPlatform.injectedMetadata.getLegacyMetadata();
@@ -187,6 +194,9 @@ const $setupBreadcrumbsAutoClear = (newPlatform: CoreStart) => (
187194
});
188195

189196
$rootScope.$on('$routeChangeSuccess', () => {
197+
if (isDummyWrapperRoute($route)) {
198+
return;
199+
}
190200
const current = $route.current || {};
191201

192202
if (breadcrumbSetSinceRouteChange || (current.$$route && current.$$route.redirectTo)) {
@@ -226,6 +236,9 @@ const $setupBadgeAutoClear = (newPlatform: CoreStart) => (
226236
});
227237

228238
$rootScope.$on('$routeChangeSuccess', () => {
239+
if (isDummyWrapperRoute($route)) {
240+
return;
241+
}
229242
const current = $route.current || {};
230243

231244
if (badgeSetSinceRouteChange || (current.$$route && current.$$route.redirectTo)) {
@@ -270,6 +283,9 @@ const $setupHelpExtensionAutoClear = (newPlatform: CoreStart) => (
270283
const $route = $injector.has('$route') ? $injector.get('$route') : {};
271284

272285
$rootScope.$on('$routeChangeStart', () => {
286+
if (isDummyWrapperRoute($route)) {
287+
return;
288+
}
273289
helpExtensionSetSinceRouteChange = false;
274290
});
275291

@@ -286,10 +302,15 @@ const $setupHelpExtensionAutoClear = (newPlatform: CoreStart) => (
286302

287303
const $setupUrlOverflowHandling = (newPlatform: CoreStart) => (
288304
$location: ILocationService,
289-
$rootScope: IRootScopeService
305+
$rootScope: IRootScopeService,
306+
$injector: auto.IInjectorService
290307
) => {
308+
const $route = $injector.has('$route') ? $injector.get('$route') : {};
291309
const urlOverflow = new UrlOverflowService();
292310
const check = () => {
311+
if (isDummyWrapperRoute($route)) {
312+
return;
313+
}
293314
// disable long url checks when storing state in session storage
294315
if (newPlatform.uiSettings.get('state:storeInSessionStorage')) {
295316
return;

src/legacy/ui/public/routes/route_manager.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ import { ChromeBreadcrumb } from '../../../../core/public';
2626
interface RouteConfiguration {
2727
controller?: string | ((...args: any[]) => void);
2828
redirectTo?: string;
29+
resolveRedirectTo?: (...args: any[]) => void;
2930
reloadOnSearch?: boolean;
31+
reloadOnUrl?: boolean;
32+
outerAngularWrapperRoute?: boolean;
3033
resolve?: object;
3134
template?: string;
3235
k7Breadcrumbs?: (...args: any[]) => ChromeBreadcrumb[];

0 commit comments

Comments
 (0)