Add application.navigateToUrl core API (#67110) (#67272)

* implement `navigateToUrl` core API

* fix lint

* review comments
This commit is contained in:
Pierre Gayvallet 2020-05-25 18:07:25 +02:00 committed by GitHub
parent b095691aff
commit 95e02f9ff9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 606 additions and 31 deletions

View file

@ -6,7 +6,7 @@
Returns an URL to a given app, including the global base path. By default, the URL is relative (/basePath/app/my-app). Use the `absolute` option to generate an absolute url (http://host:port/basePath/app/my-app)
Note that when generating absolute urls, the protocol, host and port are determined from the browser location.
Note that when generating absolute urls, the origin (protocol, host and port) are determined from the browser's location.
<b>Signature:</b>

View file

@ -22,7 +22,8 @@ export interface ApplicationStart
| Method | Description |
| --- | --- |
| [getUrlForApp(appId, options)](./kibana-plugin-core-public.applicationstart.geturlforapp.md) | Returns an URL to a given app, including the global base path. By default, the URL is relative (/basePath/app/my-app). Use the <code>absolute</code> option to generate an absolute url (http://host:port/basePath/app/my-app)<!-- -->Note that when generating absolute urls, the protocol, host and port are determined from the browser location. |
| [getUrlForApp(appId, options)](./kibana-plugin-core-public.applicationstart.geturlforapp.md) | Returns an URL to a given app, including the global base path. By default, the URL is relative (/basePath/app/my-app). Use the <code>absolute</code> option to generate an absolute url (http://host:port/basePath/app/my-app)<!-- -->Note that when generating absolute urls, the origin (protocol, host and port) are determined from the browser's location. |
| [navigateToApp(appId, options)](./kibana-plugin-core-public.applicationstart.navigatetoapp.md) | Navigate to a given app |
| [navigateToUrl(url)](./kibana-plugin-core-public.applicationstart.navigatetourl.md) | Navigate to given url, which can either be an absolute url or a relative path, in a SPA friendly way when possible.<!-- -->If all these criteria are true for the given url: - (only for absolute URLs) The origin of the URL matches the origin of the browser's current location - The pathname of the URL starts with the current basePath (eg. /mybasepath/s/my-space) - The pathname segment after the basePath matches any known application route (eg. /app/<id>/ or any application's <code>appRoute</code> configuration)<!-- -->Then a SPA navigation will be performed using <code>navigateToApp</code> using the corresponding application and path. Otherwise, fallback to a full page reload to navigate to the url using <code>window.location.assign</code> |
| [registerMountContext(contextName, provider)](./kibana-plugin-core-public.applicationstart.registermountcontext.md) | Register a context provider for application mounting. Will only be available to applications that depend on the plugin that registered this context. Deprecated, use [CoreSetup.getStartServices](./kibana-plugin-core-public.coresetup.getstartservices.md)<!-- -->. |

View file

@ -0,0 +1,45 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [ApplicationStart](./kibana-plugin-core-public.applicationstart.md) &gt; [navigateToUrl](./kibana-plugin-core-public.applicationstart.navigatetourl.md)
## ApplicationStart.navigateToUrl() method
Navigate to given url, which can either be an absolute url or a relative path, in a SPA friendly way when possible.
If all these criteria are true for the given url: - (only for absolute URLs) The origin of the URL matches the origin of the browser's current location - The pathname of the URL starts with the current basePath (eg. /mybasepath/s/my-space) - The pathname segment after the basePath matches any known application route (eg. /app/<id>/ or any application's `appRoute` configuration)
Then a SPA navigation will be performed using `navigateToApp` using the corresponding application and path. Otherwise, fallback to a full page reload to navigate to the url using `window.location.assign`
<b>Signature:</b>
```typescript
navigateToUrl(url: string): Promise<void>;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| url | <code>string</code> | an absolute url, or a relative path, to navigate to. |
<b>Returns:</b>
`Promise<void>`
## Example
```ts
// current url: `https://kibana:8080/base-path/s/my-space/app/dashboard`
// will call `application.navigateToApp('discover', { path: '/some-path?foo=bar'})`
application.navigateToUrl('https://kibana:8080/base-path/s/my-space/app/discover/some-path?foo=bar')
application.navigateToUrl('/base-path/s/my-space/app/discover/some-path?foo=bar')
// will perform a full page reload using `window.location.assign`
application.navigateToUrl('https://elsewhere:8080/base-path/s/my-space/app/discover/some-path') // origin does not match
application.navigateToUrl('/app/discover/some-path') // does not include the current basePath
application.navigateToUrl('/base-path/s/my-space/app/unknown-app/some-path') // unknown application
```

View file

@ -50,6 +50,7 @@ const createStartContractMock = (): jest.Mocked<ApplicationStart> => {
currentAppId$: currentAppId$.asObservable(),
capabilities: capabilitiesServiceMock.createStartContract().capabilities,
navigateToApp: jest.fn(),
navigateToUrl: jest.fn(),
getUrlForApp: jest.fn(),
registerMountContext: jest.fn(),
};
@ -65,6 +66,7 @@ const createInternalStartContractMock = (): jest.Mocked<InternalApplicationStart
getComponent: jest.fn(),
getUrlForApp: jest.fn(),
navigateToApp: jest.fn().mockImplementation((appId) => currentAppId$.next(appId)),
navigateToUrl: jest.fn(),
registerMountContext: jest.fn(),
};
};

View file

@ -34,3 +34,9 @@ export const createBrowserHistoryMock = jest.fn().mockReturnValue(MockHistory);
jest.doMock('history', () => ({
createBrowserHistory: createBrowserHistoryMock,
}));
export const parseAppUrlMock = jest.fn();
jest.doMock('./utils', () => ({
...jest.requireActual('./utils'),
parseAppUrl: parseAppUrlMock,
}));

View file

@ -17,6 +17,12 @@
* under the License.
*/
import {
MockCapabilitiesService,
MockHistory,
parseAppUrlMock,
} from './application_service.test.mocks';
import { createElement } from 'react';
import { BehaviorSubject, Subject } from 'rxjs';
import { bufferCount, take, takeUntil } from 'rxjs/operators';
@ -26,7 +32,6 @@ import { injectedMetadataServiceMock } from '../injected_metadata/injected_metad
import { contextServiceMock } from '../context/context_service.mock';
import { httpServiceMock } from '../http/http_service.mock';
import { overlayServiceMock } from '../overlays/overlay_service.mock';
import { MockCapabilitiesService, MockHistory } from './application_service.test.mocks';
import { MockLifecycle } from './test_types';
import { ApplicationService } from './application_service';
import { App, AppNavLinkStatus, AppStatus, AppUpdater, LegacyApp } from './types';
@ -61,6 +66,7 @@ describe('#setup()', () => {
http,
context: contextServiceMock.createSetupContract(),
injectedMetadata: injectedMetadataServiceMock.createSetupContract(),
redirectTo: jest.fn(),
};
setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(false);
startDeps = { http, overlays: overlayServiceMock.createStartContract() };
@ -466,12 +472,14 @@ describe('#setup()', () => {
describe('#start()', () => {
beforeEach(() => {
MockHistory.push.mockReset();
parseAppUrlMock.mockReset();
const http = httpServiceMock.createSetupContract({ basePath: '/base-path' });
setupDeps = {
http,
context: contextServiceMock.createSetupContract(),
injectedMetadata: injectedMetadataServiceMock.createSetupContract(),
redirectTo: jest.fn(),
};
setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(false);
startDeps = { http, overlays: overlayServiceMock.createStartContract() };
@ -779,7 +787,6 @@ describe('#start()', () => {
});
it('redirects when in legacyMode', async () => {
setupDeps.redirectTo = jest.fn();
setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(true);
service.setup(setupDeps);
@ -885,7 +892,6 @@ describe('#start()', () => {
it('sets window.location.href when navigating to legacy apps', async () => {
setupDeps.http = httpServiceMock.createSetupContract({ basePath: '/test' });
setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(true);
setupDeps.redirectTo = jest.fn();
service.setup(setupDeps);
const { navigateToApp } = await service.start(startDeps);
@ -897,7 +903,6 @@ describe('#start()', () => {
it('handles legacy apps with subapps', async () => {
setupDeps.http = httpServiceMock.createSetupContract({ basePath: '/test' });
setupDeps.injectedMetadata.getLegacyMode.mockReturnValue(true);
setupDeps.redirectTo = jest.fn();
const { registerLegacyApp } = service.setup(setupDeps);
@ -909,6 +914,30 @@ describe('#start()', () => {
expect(setupDeps.redirectTo).toHaveBeenCalledWith('/test/app/baseApp');
});
});
describe('navigateToUrl', () => {
it('calls `redirectTo` when the url is not parseable', async () => {
parseAppUrlMock.mockReturnValue(undefined);
service.setup(setupDeps);
const { navigateToUrl } = await service.start(startDeps);
await navigateToUrl('/not-an-app-path');
expect(MockHistory.push).not.toHaveBeenCalled();
expect(setupDeps.redirectTo).toHaveBeenCalledWith('/not-an-app-path');
});
it('calls `navigateToApp` when the url is an internal app link', async () => {
parseAppUrlMock.mockReturnValue({ app: 'foo', path: '/some-path' });
service.setup(setupDeps);
const { navigateToUrl } = await service.start(startDeps);
await navigateToUrl('/an-app-path');
expect(MockHistory.push).toHaveBeenCalledWith('/app/foo/some-path', undefined);
expect(setupDeps.redirectTo).not.toHaveBeenCalled();
});
});
});
describe('#stop()', () => {

View file

@ -46,17 +46,14 @@ import {
Mounter,
} from './types';
import { getLeaveAction, isConfirmAction } from './application_leave';
import { appendAppPath } from './utils';
import { appendAppPath, parseAppUrl, relativeToAbsolute } from './utils';
interface SetupDeps {
context: ContextSetup;
http: HttpSetup;
injectedMetadata: InjectedMetadataSetup;
history?: History<any>;
/**
* Only necessary for redirecting to legacy apps
* @deprecated
*/
/** Used to redirect to external urls (and legacy apps) */
redirectTo?: (path: string) => void;
}
@ -109,6 +106,7 @@ export class ApplicationService {
private history?: History<any>;
private mountContext?: IContextContainer<AppMountDeprecated>;
private navigate?: (url: string, state: any) => void;
private redirectTo?: (url: string) => void;
public setup({
context,
@ -131,7 +129,7 @@ export class ApplicationService {
this.navigate = (url, state) =>
// basePath not needed here because `history` is configured with basename
this.history ? this.history.push(url, state) : redirectTo(basePath.prepend(url));
this.redirectTo = redirectTo;
this.mountContext = context.createContextContainer();
const registerStatusUpdater = (application: string, updater$: Observable<AppUpdater>) => {
@ -278,6 +276,20 @@ export class ApplicationService {
shareReplay(1)
);
const navigateToApp: InternalApplicationStart['navigateToApp'] = async (
appId,
{ path, state }: { path?: string; state?: any } = {}
) => {
if (await this.shouldNavigate(overlays)) {
if (path === undefined) {
path = applications$.value.get(appId)?.defaultPath;
}
this.appLeaveHandlers.delete(this.currentAppId$.value!);
this.navigate!(getAppUrl(availableMounters, appId, path), state);
this.currentAppId$.next(appId);
}
};
return {
applications$,
capabilities,
@ -294,14 +306,13 @@ export class ApplicationService {
const relUrl = http.basePath.prepend(getAppUrl(availableMounters, appId, path));
return absolute ? relativeToAbsolute(relUrl) : relUrl;
},
navigateToApp: async (appId, { path, state }: { path?: string; state?: any } = {}) => {
if (await this.shouldNavigate(overlays)) {
if (path === undefined) {
path = applications$.value.get(appId)?.defaultPath;
}
this.appLeaveHandlers.delete(this.currentAppId$.value!);
this.navigate!(getAppUrl(availableMounters, appId, path), state);
this.currentAppId$.next(appId);
navigateToApp,
navigateToUrl: async (url) => {
const appInfo = parseAppUrl(url, http.basePath, this.apps);
if (appInfo) {
return navigateToApp(appInfo.app, { path: appInfo.path });
} else {
return this.redirectTo!(url);
}
},
getComponent: () => {
@ -388,10 +399,3 @@ const updateStatus = <T extends AppBase>(app: T, statusUpdaters: AppUpdaterWrapp
...changes,
};
};
function relativeToAbsolute(url: string) {
// convert all link urls to absolute urls
const a = document.createElement('a');
a.setAttribute('href', url);
return a.href;
}

View file

@ -659,12 +659,41 @@ export interface ApplicationStart {
*/
navigateToApp(appId: string, options?: { path?: string; state?: any }): Promise<void>;
/**
* Navigate to given url, which can either be an absolute url or a relative path, in a SPA friendly way when possible.
*
* If all these criteria are true for the given url:
* - (only for absolute URLs) The origin of the URL matches the origin of the browser's current location
* - The pathname of the URL starts with the current basePath (eg. /mybasepath/s/my-space)
* - The pathname segment after the basePath matches any known application route (eg. /app/<id>/ or any application's `appRoute` configuration)
*
* Then a SPA navigation will be performed using `navigateToApp` using the corresponding application and path.
* Otherwise, fallback to a full page reload to navigate to the url using `window.location.assign`
*
* @example
* ```ts
* // current url: `https://kibana:8080/base-path/s/my-space/app/dashboard`
*
* // will call `application.navigateToApp('discover', { path: '/some-path?foo=bar'})`
* application.navigateToUrl('https://kibana:8080/base-path/s/my-space/app/discover/some-path?foo=bar')
* application.navigateToUrl('/base-path/s/my-space/app/discover/some-path?foo=bar')
*
* // will perform a full page reload using `window.location.assign`
* application.navigateToUrl('https://elsewhere:8080/base-path/s/my-space/app/discover/some-path') // origin does not match
* application.navigateToUrl('/app/discover/some-path') // does not include the current basePath
* application.navigateToUrl('/base-path/s/my-space/app/unknown-app/some-path') // unknown application
* ```
*
* @param url - an absolute url, or a relative path, to navigate to.
*/
navigateToUrl(url: string): Promise<void>;
/**
* Returns an URL to a given app, including the global base path.
* By default, the URL is relative (/basePath/app/my-app).
* Use the `absolute` option to generate an absolute url (http://host:port/basePath/app/my-app)
*
* Note that when generating absolute urls, the protocol, host and port are determined from the browser location.
* Note that when generating absolute urls, the origin (protocol, host and port) are determined from the browser's location.
*
* @param appId
* @param options.path - optional path inside application to deep link to
@ -677,7 +706,6 @@ export interface ApplicationStart {
* plugin that registered this context. Deprecated, use {@link CoreSetup.getStartServices}.
*
* @deprecated
* @param pluginOpaqueId - The opaque ID of the plugin that is registering the context.
* @param contextName - The key of {@link AppMountContext} this provider's return value should be attached to.
* @param provider - A {@link IContextProvider} function
*/
@ -696,7 +724,7 @@ export interface ApplicationStart {
export interface InternalApplicationStart
extends Pick<
ApplicationStart,
'capabilities' | 'navigateToApp' | 'getUrlForApp' | 'currentAppId$'
'capabilities' | 'navigateToApp' | 'navigateToUrl' | 'getUrlForApp' | 'currentAppId$'
> {
/**
* Apps available based on the current capabilities.

View file

@ -17,7 +17,15 @@
* under the License.
*/
import { removeSlashes, appendAppPath } from './utils';
import { LegacyApp, App } from './types';
import { BasePath } from '../http/base_path';
import {
removeSlashes,
appendAppPath,
isLegacyApp,
relativeToAbsolute,
parseAppUrl,
} from './utils';
describe('removeSlashes', () => {
it('only removes duplicates by default', () => {
@ -69,3 +77,385 @@ describe('appendAppPath', () => {
expect(appendAppPath('/app/my-app', '/some-path#/hash')).toEqual('/app/my-app/some-path#/hash');
});
});
describe('isLegacyApp', () => {
it('returns true for legacy apps', () => {
expect(
isLegacyApp({
id: 'legacy',
title: 'Legacy App',
appUrl: '/some-url',
legacy: true,
})
).toEqual(true);
});
it('returns false for non-legacy apps', () => {
expect(
isLegacyApp({
id: 'legacy',
title: 'Legacy App',
mount: () => () => undefined,
legacy: false,
})
).toEqual(false);
});
});
describe('relativeToAbsolute', () => {
it('converts a relative path to an absolute url', () => {
const origin = window.location.origin;
expect(relativeToAbsolute('path')).toEqual(`${origin}/path`);
expect(relativeToAbsolute('/path#hash')).toEqual(`${origin}/path#hash`);
expect(relativeToAbsolute('/path?query=foo')).toEqual(`${origin}/path?query=foo`);
});
});
describe('parseAppUrl', () => {
let apps: Map<string, App<any> | LegacyApp>;
let basePath: BasePath;
const getOrigin = () => 'https://kibana.local:8080';
const createApp = (props: Partial<App>): App => {
const app: App = {
id: 'some-id',
title: 'some-title',
mount: () => () => undefined,
...props,
legacy: false,
};
apps.set(app.id, app);
return app;
};
const createLegacyApp = (props: Partial<LegacyApp>): LegacyApp => {
const app: LegacyApp = {
id: 'some-id',
title: 'some-title',
appUrl: '/my-url',
...props,
legacy: true,
};
apps.set(app.id, app);
return app;
};
beforeEach(() => {
apps = new Map();
basePath = new BasePath('/base-path');
createApp({
id: 'foo',
});
createApp({
id: 'bar',
appRoute: '/custom-bar',
});
createLegacyApp({
id: 'legacy',
appUrl: '/app/legacy',
});
});
describe('with relative paths', () => {
it('parses the app id', () => {
expect(parseAppUrl('/base-path/app/foo', basePath, apps, getOrigin)).toEqual({
app: 'foo',
path: undefined,
});
expect(parseAppUrl('/base-path/custom-bar', basePath, apps, getOrigin)).toEqual({
app: 'bar',
path: undefined,
});
});
it('parses the path', () => {
expect(parseAppUrl('/base-path/app/foo/some/path', basePath, apps, getOrigin)).toEqual({
app: 'foo',
path: '/some/path',
});
expect(parseAppUrl('/base-path/custom-bar/another/path/', basePath, apps, getOrigin)).toEqual(
{
app: 'bar',
path: '/another/path/',
}
);
});
it('includes query and hash in the path for default app route', () => {
expect(parseAppUrl('/base-path/app/foo#hash/bang', basePath, apps, getOrigin)).toEqual({
app: 'foo',
path: '#hash/bang',
});
expect(parseAppUrl('/base-path/app/foo?hello=dolly', basePath, apps, getOrigin)).toEqual({
app: 'foo',
path: '?hello=dolly',
});
expect(parseAppUrl('/base-path/app/foo/path?hello=dolly', basePath, apps, getOrigin)).toEqual(
{
app: 'foo',
path: '/path?hello=dolly',
}
);
expect(parseAppUrl('/base-path/app/foo/path#hash/bang', basePath, apps, getOrigin)).toEqual({
app: 'foo',
path: '/path#hash/bang',
});
expect(
parseAppUrl('/base-path/app/foo/path#hash/bang?hello=dolly', basePath, apps, getOrigin)
).toEqual({
app: 'foo',
path: '/path#hash/bang?hello=dolly',
});
});
it('includes query and hash in the path for custom app route', () => {
expect(parseAppUrl('/base-path/custom-bar#hash/bang', basePath, apps, getOrigin)).toEqual({
app: 'bar',
path: '#hash/bang',
});
expect(parseAppUrl('/base-path/custom-bar?hello=dolly', basePath, apps, getOrigin)).toEqual({
app: 'bar',
path: '?hello=dolly',
});
expect(
parseAppUrl('/base-path/custom-bar/path?hello=dolly', basePath, apps, getOrigin)
).toEqual({
app: 'bar',
path: '/path?hello=dolly',
});
expect(
parseAppUrl('/base-path/custom-bar/path#hash/bang', basePath, apps, getOrigin)
).toEqual({
app: 'bar',
path: '/path#hash/bang',
});
expect(
parseAppUrl('/base-path/custom-bar/path#hash/bang?hello=dolly', basePath, apps, getOrigin)
).toEqual({
app: 'bar',
path: '/path#hash/bang?hello=dolly',
});
});
it('works with legacy apps', () => {
expect(parseAppUrl('/base-path/app/legacy', basePath, apps, getOrigin)).toEqual({
app: 'legacy',
path: undefined,
});
expect(
parseAppUrl('/base-path/app/legacy/path#hash?query=bar', basePath, apps, getOrigin)
).toEqual({
app: 'legacy',
path: '/path#hash?query=bar',
});
});
it('returns undefined when the app is not known', () => {
expect(parseAppUrl('/base-path/app/non-registered', basePath, apps, getOrigin)).toEqual(
undefined
);
expect(parseAppUrl('/base-path/unknown-path', basePath, apps, getOrigin)).toEqual(undefined);
});
});
describe('with absolute urls', () => {
it('parses the app id', () => {
expect(
parseAppUrl('https://kibana.local:8080/base-path/app/foo', basePath, apps, getOrigin)
).toEqual({
app: 'foo',
path: undefined,
});
expect(
parseAppUrl('https://kibana.local:8080/base-path/custom-bar', basePath, apps, getOrigin)
).toEqual({
app: 'bar',
path: undefined,
});
});
it('parses the path', () => {
expect(
parseAppUrl(
'https://kibana.local:8080/base-path/app/foo/some/path',
basePath,
apps,
getOrigin
)
).toEqual({
app: 'foo',
path: '/some/path',
});
expect(
parseAppUrl(
'https://kibana.local:8080/base-path/custom-bar/another/path/',
basePath,
apps,
getOrigin
)
).toEqual({
app: 'bar',
path: '/another/path/',
});
});
it('includes query and hash in the path for default app routes', () => {
expect(
parseAppUrl(
'https://kibana.local:8080/base-path/app/foo#hash/bang',
basePath,
apps,
getOrigin
)
).toEqual({
app: 'foo',
path: '#hash/bang',
});
expect(
parseAppUrl(
'https://kibana.local:8080/base-path/app/foo?hello=dolly',
basePath,
apps,
getOrigin
)
).toEqual({
app: 'foo',
path: '?hello=dolly',
});
expect(
parseAppUrl(
'https://kibana.local:8080/base-path/app/foo/path?hello=dolly',
basePath,
apps,
getOrigin
)
).toEqual({
app: 'foo',
path: '/path?hello=dolly',
});
expect(
parseAppUrl(
'https://kibana.local:8080/base-path/app/foo/path#hash/bang',
basePath,
apps,
getOrigin
)
).toEqual({
app: 'foo',
path: '/path#hash/bang',
});
expect(
parseAppUrl(
'https://kibana.local:8080/base-path/app/foo/path#hash/bang?hello=dolly',
basePath,
apps,
getOrigin
)
).toEqual({
app: 'foo',
path: '/path#hash/bang?hello=dolly',
});
});
it('includes query and hash in the path for custom app route', () => {
expect(
parseAppUrl(
'https://kibana.local:8080/base-path/custom-bar#hash/bang',
basePath,
apps,
getOrigin
)
).toEqual({
app: 'bar',
path: '#hash/bang',
});
expect(
parseAppUrl(
'https://kibana.local:8080/base-path/custom-bar?hello=dolly',
basePath,
apps,
getOrigin
)
).toEqual({
app: 'bar',
path: '?hello=dolly',
});
expect(
parseAppUrl(
'https://kibana.local:8080/base-path/custom-bar/path?hello=dolly',
basePath,
apps,
getOrigin
)
).toEqual({
app: 'bar',
path: '/path?hello=dolly',
});
expect(
parseAppUrl(
'https://kibana.local:8080/base-path/custom-bar/path#hash/bang',
basePath,
apps,
getOrigin
)
).toEqual({
app: 'bar',
path: '/path#hash/bang',
});
expect(
parseAppUrl(
'https://kibana.local:8080/base-path/custom-bar/path#hash/bang?hello=dolly',
basePath,
apps,
getOrigin
)
).toEqual({
app: 'bar',
path: '/path#hash/bang?hello=dolly',
});
});
it('works with legacy apps', () => {
expect(
parseAppUrl('https://kibana.local:8080/base-path/app/legacy', basePath, apps, getOrigin)
).toEqual({
app: 'legacy',
path: undefined,
});
expect(
parseAppUrl(
'https://kibana.local:8080/base-path/app/legacy/path#hash?query=bar',
basePath,
apps,
getOrigin
)
).toEqual({
app: 'legacy',
path: '/path#hash?query=bar',
});
});
it('returns undefined when the app is not known', () => {
expect(
parseAppUrl(
'https://kibana.local:8080/base-path/app/non-registered',
basePath,
apps,
getOrigin
)
).toEqual(undefined);
expect(
parseAppUrl('https://kibana.local:8080/base-path/unknown-path', basePath, apps, getOrigin)
).toEqual(undefined);
});
it('returns undefined when origin does not match', () => {
expect(
parseAppUrl(
'https://other-kibana.external:8080/base-path/app/foo',
basePath,
apps,
getOrigin
)
).toEqual(undefined);
expect(
parseAppUrl(
'https://other-kibana.external:8080/base-path/custom-bar',
basePath,
apps,
getOrigin
)
).toEqual(undefined);
});
});
});

View file

@ -17,6 +17,14 @@
* under the License.
*/
import { IBasePath } from '../http';
import { App, LegacyApp } from './types';
export interface AppUrlInfo {
app: string;
path?: string;
}
/**
* Utility to remove trailing, leading or duplicate slashes.
* By default will only remove duplicates.
@ -52,3 +60,62 @@ export const appendAppPath = (appBasePath: string, path: string = '') => {
leading: false,
});
};
export function isLegacyApp(app: App | LegacyApp): app is LegacyApp {
return app.legacy === true;
}
/**
* Converts a relative path to an absolute url.
* Implementation is based on a specified behavior of the browser to automatically convert
* a relative url to an absolute one when setting the `href` attribute of a `<a>` html element.
*
* @example
* ```ts
* // current url: `https://kibana:8000/base-path/app/my-app`
* relativeToAbsolute('/base-path/app/another-app') => `https://kibana:8000/base-path/app/another-app`
* ```
*/
export const relativeToAbsolute = (url: string): string => {
const a = document.createElement('a');
a.setAttribute('href', url);
return a.href;
};
/**
* Parse given url and return the associated app id and path if any app matches.
* Input can either be:
* - a path containing the basePath, ie `/base-path/app/my-app/some-path`
* - an absolute url matching the `origin` of the kibana instance (as seen by the browser),
* i.e `https://kibana:8080/base-path/app/my-app/some-path`
*/
export const parseAppUrl = (
url: string,
basePath: IBasePath,
apps: Map<string, App<unknown> | LegacyApp>,
getOrigin: () => string = () => window.location.origin
): AppUrlInfo | undefined => {
url = removeBasePath(url, basePath, getOrigin());
if (!url.startsWith('/')) {
return undefined;
}
for (const app of apps.values()) {
const appPath = isLegacyApp(app) ? app.appUrl : app.appRoute || `/app/${app.id}`;
if (url.startsWith(appPath)) {
const path = url.substr(appPath.length);
return {
app: app.id,
path: path.length ? path : undefined,
};
}
}
};
const removeBasePath = (url: string, basePath: IBasePath, origin: string): string => {
if (url.startsWith(origin)) {
url = url.substring(origin.length);
}
return basePath.remove(url);
};

View file

@ -135,6 +135,7 @@ export class LegacyPlatformService {
capabilities: core.application.capabilities,
getUrlForApp: core.application.getUrlForApp,
navigateToApp: core.application.navigateToApp,
navigateToUrl: core.application.navigateToUrl,
registerMountContext: notSupported(`core.application.registerMountContext()`),
},
};

View file

@ -138,6 +138,7 @@ export function createPluginStartContext<
currentAppId$: deps.application.currentAppId$,
capabilities: deps.application.capabilities,
navigateToApp: deps.application.navigateToApp,
navigateToUrl: deps.application.navigateToUrl,
getUrlForApp: deps.application.getUrlForApp,
registerMountContext: (contextName, provider) =>
deps.application.registerMountContext(plugin.opaqueId, contextName, provider),

View file

@ -116,6 +116,7 @@ export interface ApplicationStart {
path?: string;
state?: any;
}): Promise<void>;
navigateToUrl(url: string): Promise<void>;
// @deprecated
registerMountContext<T extends keyof AppMountContext>(contextName: T, provider: IContextProvider<AppMountDeprecated, T>): void;
}