mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
* implement `navigateToUrl` core API * fix lint * review comments
This commit is contained in:
parent
b095691aff
commit
95e02f9ff9
13 changed files with 606 additions and 31 deletions
|
@ -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>
|
||||
|
||||
|
|
|
@ -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)<!-- -->. |
|
||||
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ApplicationStart](./kibana-plugin-core-public.applicationstart.md) > [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
|
||||
|
||||
```
|
||||
|
|
@ -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(),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
}));
|
||||
|
|
|
@ -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()', () => {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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()`),
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue