diff --git a/docs/development/core/public/kibana-plugin-core-public.applicationstart.geturlforapp.md b/docs/development/core/public/kibana-plugin-core-public.applicationstart.geturlforapp.md
index 055ad9f37e654..1eaf00c7a678d 100644
--- a/docs/development/core/public/kibana-plugin-core-public.applicationstart.geturlforapp.md
+++ b/docs/development/core/public/kibana-plugin-core-public.applicationstart.geturlforapp.md
@@ -4,9 +4,11 @@
## ApplicationStart.getUrlForApp() method
-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)
+Returns the absolute path (or URL) to a given app, including the global base path.
-Note that when generating absolute urls, the origin (protocol, host and port) are determined from the browser's location.
+By default, it returns the absolute path of the application (e.g `/basePath/app/my-app`). Use the `absolute` option to generate an absolute url instead (e.g `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 current location.
Signature:
diff --git a/docs/development/core/public/kibana-plugin-core-public.applicationstart.md b/docs/development/core/public/kibana-plugin-core-public.applicationstart.md
index 00318f32984e9..ae62a7767a0e9 100644
--- a/docs/development/core/public/kibana-plugin-core-public.applicationstart.md
+++ b/docs/development/core/public/kibana-plugin-core-public.applicationstart.md
@@ -23,8 +23,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 absolute
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. |
+| [getUrlForApp(appId, options)](./kibana-plugin-core-public.applicationstart.geturlforapp.md) | Returns the absolute path (or URL) to a given app, including the global base path.By default, it returns the absolute path of the application (e.g /basePath/app/my-app
). Use the absolute
option to generate an absolute url instead (e.g 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 current 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// 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
|
+| [navigateToUrl(url)](./kibana-plugin-core-public.applicationstart.navigatetourl.md) | Navigate to given URL in a SPA friendly way when possible (when the URL will redirect to a valid application within the current basePath).The method resolves pathnames the same way browsers do when resolving a <a href>
value. The provided url
can be: - an absolute URL - an absolute path - a path relative to the current URL (window.location.href)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 resolved pathname of the provided URL/path starts with the current basePath (eg. /mybasepath/s/my-space) - The pathname segment after the basePath matches any known application route (eg. /app// 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
|
| [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). |
diff --git a/docs/development/core/public/kibana-plugin-core-public.applicationstart.navigatetourl.md b/docs/development/core/public/kibana-plugin-core-public.applicationstart.navigatetourl.md
index 86b86776b0b12..8639394cbc421 100644
--- a/docs/development/core/public/kibana-plugin-core-public.applicationstart.navigatetourl.md
+++ b/docs/development/core/public/kibana-plugin-core-public.applicationstart.navigatetourl.md
@@ -4,9 +4,11 @@
## 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.
+Navigate to given URL in a SPA friendly way when possible (when the URL will redirect to a valid application within the current basePath).
-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// or any application's `appRoute` configuration)
+The method resolves pathnames the same way browsers do when resolving a `` value. The provided `url` can be: - an absolute URL - an absolute path - a path relative to the current URL (window.location.href)
+
+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 resolved pathname of the provided URL/path starts with the current basePath (eg. /mybasepath/s/my-space) - The pathname segment after the basePath matches any known application route (eg. /app// 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`
@@ -20,7 +22,7 @@ navigateToUrl(url: string): Promise;
| Parameter | Type | Description |
| --- | --- | --- |
-| url | string
| an absolute url, or a relative path, to navigate to. |
+| url | string
| an absolute URL, an absolute path or a relative path, to navigate to. |
Returns:
@@ -35,11 +37,14 @@ navigateToUrl(url: string): Promise;
// 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')
+application.navigateToUrl('./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
+application.navigateToUrl('../discover') // resolve to `/base-path/s/my-space/discover` which is not a path of a known app.
+application.navigateToUrl('../../other-space/discover') // resolve to `/base-path/s/other-space/discover` which is not within the current basePath.
```
diff --git a/packages/kbn-std/src/index.ts b/packages/kbn-std/src/index.ts
index 7cf70a0e28e2c..d9d3ec4b0d52b 100644
--- a/packages/kbn-std/src/index.ts
+++ b/packages/kbn-std/src/index.ts
@@ -24,6 +24,6 @@ export { mapToObject } from './map_to_object';
export { merge } from './merge';
export { pick } from './pick';
export { withTimeout } from './promise';
-export { isRelativeUrl, modifyUrl, URLMeaningfulParts } from './url';
+export { isRelativeUrl, modifyUrl, getUrlOrigin, URLMeaningfulParts } from './url';
export { unset } from './unset';
export { getFlattenedObject } from './get_flattened_object';
diff --git a/packages/kbn-std/src/url.test.ts b/packages/kbn-std/src/url.test.ts
index 7e9b6adfd3f49..4d5c5a1808c55 100644
--- a/packages/kbn-std/src/url.test.ts
+++ b/packages/kbn-std/src/url.test.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-import { modifyUrl, isRelativeUrl } from './url';
+import { modifyUrl, isRelativeUrl, getUrlOrigin } from './url';
describe('modifyUrl()', () => {
test('throws an error with invalid input', () => {
@@ -83,3 +83,27 @@ describe('isRelativeUrl()', () => {
expect(isRelativeUrl(' //evil.com')).toBe(false);
});
});
+
+describe('getOrigin', () => {
+ describe('when passing an absolute url', () => {
+ it('return origin without port when the url does not have a port', () => {
+ expect(getUrlOrigin('https://example.com/file/to/path?example')).toEqual(
+ 'https://example.com'
+ );
+ });
+ it('return origin with port when the url does have a port', () => {
+ expect(getUrlOrigin('http://example.com:80/path/to/file')).toEqual('http://example.com:80');
+ });
+ });
+ describe('when passing a non absolute url', () => {
+ it('returns null for relative url', () => {
+ expect(getUrlOrigin('./path/to/file')).toBeNull();
+ });
+ it('returns null for absolute path', () => {
+ expect(getUrlOrigin('/path/to/file')).toBeNull();
+ });
+ it('returns null for empty url', () => {
+ expect(getUrlOrigin('')).toBeNull();
+ });
+ });
+});
diff --git a/packages/kbn-std/src/url.ts b/packages/kbn-std/src/url.ts
index edcdebbd2bc81..745ed05751b10 100644
--- a/packages/kbn-std/src/url.ts
+++ b/packages/kbn-std/src/url.ts
@@ -125,3 +125,14 @@ export function isRelativeUrl(candidatePath: string) {
}
return true;
}
+
+/**
+ * Returns the origin (protocol + host + port) from given `url` if `url` is a valid absolute url, or null otherwise
+ */
+export function getUrlOrigin(url: string): string | null {
+ const obj = parseUrl(url);
+ if (!obj.protocol && !obj.hostname) {
+ return null;
+ }
+ return `${obj.protocol}//${obj.hostname}${obj.port ? `:${obj.port}` : ''}`;
+}
diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts
index df83b6e932aad..02d2d3a52a01a 100644
--- a/src/core/public/application/types.ts
+++ b/src/core/public/application/types.ts
@@ -710,11 +710,17 @@ export interface ApplicationStart {
navigateToApp(appId: string, options?: NavigateToAppOptions): Promise;
/**
- * Navigate to given url, which can either be an absolute url or a relative path, in a SPA friendly way when possible.
+ * Navigate to given URL in a SPA friendly way when possible (when the URL will redirect to a valid application
+ * within the current basePath).
*
- * If all these criteria are true for the given url:
+ * The method resolves pathnames the same way browsers do when resolving a `` value. The provided `url` can be:
+ * - an absolute URL
+ * - an absolute path
+ * - a path relative to the current URL (window.location.href)
+ *
+ * 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 resolved pathname of the provided URL/path starts with the current basePath (eg. /mybasepath/s/my-space)
* - The pathname segment after the basePath matches any known application route (eg. /app// or any application's `appRoute` configuration)
*
* Then a SPA navigation will be performed using `navigateToApp` using the corresponding application and path.
@@ -727,23 +733,27 @@ export interface ApplicationStart {
* // 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')
+ * application.navigateToUrl('./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
+ * application.navigateToUrl('../discover') // resolve to `/base-path/s/my-space/discover` which is not a path of a known app.
+ * application.navigateToUrl('../../other-space/discover') // resolve to `/base-path/s/other-space/discover` which is not within the current basePath.
* ```
*
- * @param url - an absolute url, or a relative path, to navigate to.
+ * @param url - an absolute URL, an absolute path or a relative path, to navigate to.
*/
navigateToUrl(url: string): Promise;
/**
- * 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)
+ * Returns the absolute path (or URL) to a given app, including the global base path.
+ *
+ * By default, it returns the absolute path of the application (e.g `/basePath/app/my-app`).
+ * Use the `absolute` option to generate an absolute url instead (e.g `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.
+ * Note that when generating absolute urls, the origin (protocol, host and port) are determined from the browser's current location.
*
* @param appId
* @param options.path - optional path inside application to deep link to
diff --git a/src/core/public/application/utils.ts b/src/core/public/application/utils.ts
deleted file mode 100644
index 85760526bf544..0000000000000
--- a/src/core/public/application/utils.ts
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * 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 { IBasePath } from '../http';
-import { App, AppNavLinkStatus, AppStatus, ParsedAppUrl, PublicAppInfo } from './types';
-
-/**
- * Utility to remove trailing, leading or duplicate slashes.
- * By default will only remove duplicates.
- */
-export const removeSlashes = (
- url: string,
- {
- trailing = false,
- leading = false,
- duplicates = true,
- }: { trailing?: boolean; leading?: boolean; duplicates?: boolean } = {}
-): string => {
- if (duplicates) {
- url = url.replace(/\/{2,}/g, '/');
- }
- if (trailing) {
- url = url.replace(/\/$/, '');
- }
- if (leading) {
- url = url.replace(/^\//, '');
- }
- return url;
-};
-
-export const appendAppPath = (appBasePath: string, path: string = '') => {
- // Only prepend slash if not a hash or query path
- path = path === '' || path.startsWith('#') || path.startsWith('?') ? path : `/${path}`;
- // Do not remove trailing slash when in hashbang or basePath
- const removeTrailing = path.indexOf('#') === -1 && appBasePath.indexOf('#') === -1;
- return removeSlashes(`${appBasePath}${path}`, {
- trailing: removeTrailing,
- duplicates: true,
- leading: false,
- });
-};
-
-/**
- * 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 `` 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>,
- getOrigin: () => string = () => window.location.origin
-): ParsedAppUrl | undefined => {
- url = removeBasePath(url, basePath, getOrigin());
- if (!url.startsWith('/')) {
- return undefined;
- }
-
- for (const app of apps.values()) {
- const appPath = 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);
-};
-
-export function getAppInfo(app: App): PublicAppInfo {
- const navLinkStatus =
- app.navLinkStatus === AppNavLinkStatus.default
- ? app.status === AppStatus.inaccessible
- ? AppNavLinkStatus.hidden
- : AppNavLinkStatus.visible
- : app.navLinkStatus!;
- const { updater$, mount, ...infos } = app;
- return {
- ...infos,
- status: app.status!,
- navLinkStatus,
- appRoute: app.appRoute!,
- };
-}
diff --git a/src/core/public/application/utils/append_app_path.test.ts b/src/core/public/application/utils/append_app_path.test.ts
new file mode 100644
index 0000000000000..a153b5753bbe2
--- /dev/null
+++ b/src/core/public/application/utils/append_app_path.test.ts
@@ -0,0 +1,40 @@
+/*
+ * 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 { appendAppPath } from './append_app_path';
+
+describe('appendAppPath', () => {
+ it('appends the appBasePath with given path', () => {
+ expect(appendAppPath('/app/my-app', '/some-path')).toEqual('/app/my-app/some-path');
+ expect(appendAppPath('/app/my-app/', 'some-path')).toEqual('/app/my-app/some-path');
+ expect(appendAppPath('/app/my-app', 'some-path')).toEqual('/app/my-app/some-path');
+ expect(appendAppPath('/app/my-app', '')).toEqual('/app/my-app');
+ });
+
+ it('preserves the trailing slash only if included in the hash or appPath', () => {
+ expect(appendAppPath('/app/my-app', '/some-path/')).toEqual('/app/my-app/some-path');
+ expect(appendAppPath('/app/my-app', '/some-path#/')).toEqual('/app/my-app/some-path#/');
+ expect(appendAppPath('/app/my-app#/', '')).toEqual('/app/my-app#/');
+ expect(appendAppPath('/app/my-app#', '/')).toEqual('/app/my-app#/');
+ expect(appendAppPath('/app/my-app', '/some-path#/hash/')).toEqual(
+ '/app/my-app/some-path#/hash/'
+ );
+ expect(appendAppPath('/app/my-app', '/some-path#/hash')).toEqual('/app/my-app/some-path#/hash');
+ });
+});
diff --git a/src/core/public/application/utils/append_app_path.ts b/src/core/public/application/utils/append_app_path.ts
new file mode 100644
index 0000000000000..70cb4a44c648e
--- /dev/null
+++ b/src/core/public/application/utils/append_app_path.ts
@@ -0,0 +1,32 @@
+/*
+ * 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 { removeSlashes } from './remove_slashes';
+
+export const appendAppPath = (appBasePath: string, path: string = '') => {
+ // Only prepend slash if not a hash or query path
+ path = path === '' || path.startsWith('#') || path.startsWith('?') ? path : `/${path}`;
+ // Do not remove trailing slash when in hashbang or basePath
+ const removeTrailing = path.indexOf('#') === -1 && appBasePath.indexOf('#') === -1;
+ return removeSlashes(`${appBasePath}${path}`, {
+ trailing: removeTrailing,
+ duplicates: true,
+ leading: false,
+ });
+};
diff --git a/src/core/public/application/utils/get_app_info.test.ts b/src/core/public/application/utils/get_app_info.test.ts
new file mode 100644
index 0000000000000..055f7d1a5ada9
--- /dev/null
+++ b/src/core/public/application/utils/get_app_info.test.ts
@@ -0,0 +1,75 @@
+/*
+ * 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 { of } from 'rxjs';
+import { App, AppNavLinkStatus, AppStatus } from '../types';
+import { getAppInfo } from './get_app_info';
+
+describe('getAppInfo', () => {
+ const createApp = (props: Partial = {}): App => ({
+ mount: () => () => undefined,
+ updater$: of(() => undefined),
+ id: 'some-id',
+ title: 'some-title',
+ status: AppStatus.accessible,
+ navLinkStatus: AppNavLinkStatus.default,
+ appRoute: `/app/some-id`,
+ ...props,
+ });
+
+ it('converts an application and remove sensitive properties', () => {
+ const app = createApp();
+ const info = getAppInfo(app);
+
+ expect(info).toEqual({
+ id: 'some-id',
+ title: 'some-title',
+ status: AppStatus.accessible,
+ navLinkStatus: AppNavLinkStatus.visible,
+ appRoute: `/app/some-id`,
+ });
+ });
+
+ it('computes the navLinkStatus depending on the app status', () => {
+ expect(
+ getAppInfo(
+ createApp({
+ navLinkStatus: AppNavLinkStatus.default,
+ status: AppStatus.inaccessible,
+ })
+ )
+ ).toEqual(
+ expect.objectContaining({
+ navLinkStatus: AppNavLinkStatus.hidden,
+ })
+ );
+ expect(
+ getAppInfo(
+ createApp({
+ navLinkStatus: AppNavLinkStatus.default,
+ status: AppStatus.accessible,
+ })
+ )
+ ).toEqual(
+ expect.objectContaining({
+ navLinkStatus: AppNavLinkStatus.visible,
+ })
+ );
+ });
+});
diff --git a/src/core/public/application/utils/get_app_info.ts b/src/core/public/application/utils/get_app_info.ts
new file mode 100644
index 0000000000000..71cd8a3e14929
--- /dev/null
+++ b/src/core/public/application/utils/get_app_info.ts
@@ -0,0 +1,36 @@
+/*
+ * 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, AppNavLinkStatus, AppStatus, PublicAppInfo } from '../types';
+
+export function getAppInfo(app: App): PublicAppInfo {
+ const navLinkStatus =
+ app.navLinkStatus === AppNavLinkStatus.default
+ ? app.status === AppStatus.inaccessible
+ ? AppNavLinkStatus.hidden
+ : AppNavLinkStatus.visible
+ : app.navLinkStatus!;
+ const { updater$, mount, ...infos } = app;
+ return {
+ ...infos,
+ status: app.status!,
+ navLinkStatus,
+ appRoute: app.appRoute!,
+ };
+}
diff --git a/src/core/public/application/utils/index.ts b/src/core/public/application/utils/index.ts
new file mode 100644
index 0000000000000..3b8a34df8c50d
--- /dev/null
+++ b/src/core/public/application/utils/index.ts
@@ -0,0 +1,24 @@
+/*
+ * 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 { appendAppPath } from './append_app_path';
+export { getAppInfo } from './get_app_info';
+export { parseAppUrl } from './parse_app_url';
+export { relativeToAbsolute } from './relative_to_absolute';
+export { removeSlashes } from './remove_slashes';
diff --git a/src/core/public/application/utils.test.ts b/src/core/public/application/utils/parse_app_url.test.ts
similarity index 58%
rename from src/core/public/application/utils.test.ts
rename to src/core/public/application/utils/parse_app_url.test.ts
index ee1d82a7a872e..bf7e0a88a0742 100644
--- a/src/core/public/application/utils.test.ts
+++ b/src/core/public/application/utils/parse_app_url.test.ts
@@ -17,78 +17,16 @@
* under the License.
*/
-import { of } from 'rxjs';
-import { App, AppNavLinkStatus, AppStatus } from './types';
-import { BasePath } from '../http/base_path';
-import { appendAppPath, getAppInfo, parseAppUrl, relativeToAbsolute, removeSlashes } from './utils';
-
-describe('removeSlashes', () => {
- it('only removes duplicates by default', () => {
- expect(removeSlashes('/some//url//to//')).toEqual('/some/url/to/');
- expect(removeSlashes('some/////other//url')).toEqual('some/other/url');
- });
-
- it('remove trailing slash when `trailing` is true', () => {
- expect(removeSlashes('/some//url//to//', { trailing: true })).toEqual('/some/url/to');
- });
-
- it('remove leading slash when `leading` is true', () => {
- expect(removeSlashes('/some//url//to//', { leading: true })).toEqual('some/url/to/');
- });
-
- it('does not removes duplicates when `duplicates` is false', () => {
- expect(removeSlashes('/some//url//to/', { leading: true, duplicates: false })).toEqual(
- 'some//url//to/'
- );
- expect(removeSlashes('/some//url//to/', { trailing: true, duplicates: false })).toEqual(
- '/some//url//to'
- );
- });
-
- it('accept mixed options', () => {
- expect(
- removeSlashes('/some//url//to/', { leading: true, duplicates: false, trailing: true })
- ).toEqual('some//url//to');
- expect(
- removeSlashes('/some//url//to/', { leading: true, duplicates: true, trailing: true })
- ).toEqual('some/url/to');
- });
-});
-
-describe('appendAppPath', () => {
- it('appends the appBasePath with given path', () => {
- expect(appendAppPath('/app/my-app', '/some-path')).toEqual('/app/my-app/some-path');
- expect(appendAppPath('/app/my-app/', 'some-path')).toEqual('/app/my-app/some-path');
- expect(appendAppPath('/app/my-app', 'some-path')).toEqual('/app/my-app/some-path');
- expect(appendAppPath('/app/my-app', '')).toEqual('/app/my-app');
- });
-
- it('preserves the trailing slash only if included in the hash or appPath', () => {
- expect(appendAppPath('/app/my-app', '/some-path/')).toEqual('/app/my-app/some-path');
- expect(appendAppPath('/app/my-app', '/some-path#/')).toEqual('/app/my-app/some-path#/');
- expect(appendAppPath('/app/my-app#/', '')).toEqual('/app/my-app#/');
- expect(appendAppPath('/app/my-app#', '/')).toEqual('/app/my-app#/');
- expect(appendAppPath('/app/my-app', '/some-path#/hash/')).toEqual(
- '/app/my-app/some-path#/hash/'
- );
- expect(appendAppPath('/app/my-app', '/some-path#/hash')).toEqual('/app/my-app/some-path#/hash');
- });
-});
-
-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`);
- });
-});
+import { App } from '../types';
+import { BasePath } from '../../http/base_path';
+import { parseAppUrl } from './parse_app_url';
describe('parseAppUrl', () => {
let apps: Map>;
let basePath: BasePath;
- const getOrigin = () => 'https://kibana.local:8080';
+ const currentUrl =
+ 'https://kibana.local:8080/base-path/app/current/current-path?current-query=true';
const createApp = (props: Partial): App => {
const app: App = {
@@ -114,101 +52,178 @@ describe('parseAppUrl', () => {
});
});
- describe('with relative paths', () => {
+ describe('with absolute paths', () => {
it('parses the app id', () => {
- expect(parseAppUrl('/base-path/app/foo', basePath, apps, getOrigin)).toEqual({
+ expect(parseAppUrl('/base-path/app/foo', basePath, apps, currentUrl)).toEqual({
app: 'foo',
path: undefined,
});
- expect(parseAppUrl('/base-path/custom-bar', basePath, apps, getOrigin)).toEqual({
+ expect(parseAppUrl('/base-path/custom-bar', basePath, apps, currentUrl)).toEqual({
app: 'bar',
path: undefined,
});
});
it('parses the path', () => {
- expect(parseAppUrl('/base-path/app/foo/some/path', basePath, apps, getOrigin)).toEqual({
+ expect(parseAppUrl('/base-path/app/foo/some/path', basePath, apps, currentUrl)).toEqual({
app: 'foo',
path: '/some/path',
});
- expect(parseAppUrl('/base-path/custom-bar/another/path/', basePath, apps, getOrigin)).toEqual(
- {
- app: 'bar',
- path: '/another/path/',
- }
- );
+ expect(
+ parseAppUrl('/base-path/custom-bar/another/path/', basePath, apps, currentUrl)
+ ).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({
+ expect(parseAppUrl('/base-path/app/foo#hash/bang', basePath, apps, currentUrl)).toEqual({
app: 'foo',
path: '#hash/bang',
});
- expect(parseAppUrl('/base-path/app/foo?hello=dolly', basePath, apps, getOrigin)).toEqual({
+ expect(parseAppUrl('/base-path/app/foo?hello=dolly', basePath, apps, currentUrl)).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({
+ expect(
+ parseAppUrl('/base-path/app/foo/path?hello=dolly', basePath, apps, currentUrl)
+ ).toEqual({
+ app: 'foo',
+ path: '/path?hello=dolly',
+ });
+ expect(parseAppUrl('/base-path/app/foo/path#hash/bang', basePath, apps, currentUrl)).toEqual({
app: 'foo',
path: '/path#hash/bang',
});
expect(
- parseAppUrl('/base-path/app/foo/path#hash/bang?hello=dolly', basePath, apps, getOrigin)
+ parseAppUrl('/base-path/app/foo/path#hash/bang?hello=dolly', basePath, apps, currentUrl)
).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({
+ expect(parseAppUrl('/base-path/custom-bar#hash/bang', basePath, apps, currentUrl)).toEqual({
app: 'bar',
path: '#hash/bang',
});
- expect(parseAppUrl('/base-path/custom-bar?hello=dolly', basePath, apps, getOrigin)).toEqual({
+ expect(parseAppUrl('/base-path/custom-bar?hello=dolly', basePath, apps, currentUrl)).toEqual({
app: 'bar',
path: '?hello=dolly',
});
expect(
- parseAppUrl('/base-path/custom-bar/path?hello=dolly', basePath, apps, getOrigin)
+ parseAppUrl('/base-path/custom-bar/path?hello=dolly', basePath, apps, currentUrl)
).toEqual({
app: 'bar',
path: '/path?hello=dolly',
});
expect(
- parseAppUrl('/base-path/custom-bar/path#hash/bang', basePath, apps, getOrigin)
+ parseAppUrl('/base-path/custom-bar/path#hash/bang', basePath, apps, currentUrl)
).toEqual({
app: 'bar',
path: '/path#hash/bang',
});
expect(
- parseAppUrl('/base-path/custom-bar/path#hash/bang?hello=dolly', basePath, apps, getOrigin)
+ parseAppUrl('/base-path/custom-bar/path#hash/bang?hello=dolly', basePath, apps, currentUrl)
).toEqual({
app: 'bar',
path: '/path#hash/bang?hello=dolly',
});
});
it('returns undefined when the app is not known', () => {
- expect(parseAppUrl('/base-path/app/non-registered', basePath, apps, getOrigin)).toEqual(
+ expect(parseAppUrl('/base-path/app/non-registered', basePath, apps, currentUrl)).toEqual(
undefined
);
- expect(parseAppUrl('/base-path/unknown-path', basePath, apps, getOrigin)).toEqual(undefined);
+ expect(parseAppUrl('/base-path/unknown-path', basePath, apps, currentUrl)).toEqual(undefined);
+ });
+ it('returns undefined when the path does not start with the base path', () => {
+ expect(parseAppUrl('/app/foo', basePath, apps, currentUrl)).toBeUndefined();
+ });
+ });
+
+ describe('with relative paths', () => {
+ it('works with sibling relative urls', () => {
+ expect(
+ parseAppUrl('./foo', basePath, apps, 'https://kibana.local:8080/base-path/app/current')
+ ).toEqual({
+ app: 'foo',
+ path: undefined,
+ });
+ });
+ it('works with parent relative urls', () => {
+ expect(
+ parseAppUrl(
+ '../custom-bar',
+ basePath,
+ apps,
+ 'https://kibana.local:8080/base-path/app/current'
+ )
+ ).toEqual({
+ app: 'bar',
+ path: undefined,
+ });
+ });
+ it('works with nested parents', () => {
+ expect(
+ parseAppUrl(
+ '../../custom-bar',
+ basePath,
+ apps,
+ 'https://kibana.local:8080/base-path/app/current/some-path'
+ )
+ ).toEqual({
+ app: 'bar',
+ path: undefined,
+ });
+ });
+ it('parses the path', () => {
+ expect(
+ parseAppUrl(
+ './foo/path?hello=dolly',
+ basePath,
+ apps,
+ 'https://kibana.local:8080/base-path/app/current'
+ )
+ ).toEqual({
+ app: 'foo',
+ path: '/path?hello=dolly',
+ });
+ });
+ it('parses the path with query and hash', () => {
+ expect(
+ parseAppUrl(
+ '../custom-bar/path#hash?hello=dolly',
+ basePath,
+ apps,
+ 'https://kibana.local:8080/base-path/app/current'
+ )
+ ).toEqual({
+ app: 'bar',
+ path: '/path#hash?hello=dolly',
+ });
+ });
+
+ it('returns undefined if the relative path redirect outside of the basePath', () => {
+ expect(
+ parseAppUrl(
+ '../../custom-bar',
+ basePath,
+ apps,
+ 'https://kibana.local:8080/base-path/app/current'
+ )
+ ).toBeUndefined();
});
});
describe('with absolute urls', () => {
it('parses the app id', () => {
expect(
- parseAppUrl('https://kibana.local:8080/base-path/app/foo', basePath, apps, getOrigin)
+ parseAppUrl('https://kibana.local:8080/base-path/app/foo', basePath, apps, currentUrl)
).toEqual({
app: 'foo',
path: undefined,
});
expect(
- parseAppUrl('https://kibana.local:8080/base-path/custom-bar', basePath, apps, getOrigin)
+ parseAppUrl('https://kibana.local:8080/base-path/custom-bar', basePath, apps, currentUrl)
).toEqual({
app: 'bar',
path: undefined,
@@ -220,7 +235,7 @@ describe('parseAppUrl', () => {
'https://kibana.local:8080/base-path/app/foo/some/path',
basePath,
apps,
- getOrigin
+ currentUrl
)
).toEqual({
app: 'foo',
@@ -231,7 +246,7 @@ describe('parseAppUrl', () => {
'https://kibana.local:8080/base-path/custom-bar/another/path/',
basePath,
apps,
- getOrigin
+ currentUrl
)
).toEqual({
app: 'bar',
@@ -244,7 +259,7 @@ describe('parseAppUrl', () => {
'https://kibana.local:8080/base-path/app/foo#hash/bang',
basePath,
apps,
- getOrigin
+ currentUrl
)
).toEqual({
app: 'foo',
@@ -255,7 +270,7 @@ describe('parseAppUrl', () => {
'https://kibana.local:8080/base-path/app/foo?hello=dolly',
basePath,
apps,
- getOrigin
+ currentUrl
)
).toEqual({
app: 'foo',
@@ -266,7 +281,7 @@ describe('parseAppUrl', () => {
'https://kibana.local:8080/base-path/app/foo/path?hello=dolly',
basePath,
apps,
- getOrigin
+ currentUrl
)
).toEqual({
app: 'foo',
@@ -277,7 +292,7 @@ describe('parseAppUrl', () => {
'https://kibana.local:8080/base-path/app/foo/path#hash/bang',
basePath,
apps,
- getOrigin
+ currentUrl
)
).toEqual({
app: 'foo',
@@ -288,7 +303,7 @@ describe('parseAppUrl', () => {
'https://kibana.local:8080/base-path/app/foo/path#hash/bang?hello=dolly',
basePath,
apps,
- getOrigin
+ currentUrl
)
).toEqual({
app: 'foo',
@@ -301,7 +316,7 @@ describe('parseAppUrl', () => {
'https://kibana.local:8080/base-path/custom-bar#hash/bang',
basePath,
apps,
- getOrigin
+ currentUrl
)
).toEqual({
app: 'bar',
@@ -312,7 +327,7 @@ describe('parseAppUrl', () => {
'https://kibana.local:8080/base-path/custom-bar?hello=dolly',
basePath,
apps,
- getOrigin
+ currentUrl
)
).toEqual({
app: 'bar',
@@ -323,7 +338,7 @@ describe('parseAppUrl', () => {
'https://kibana.local:8080/base-path/custom-bar/path?hello=dolly',
basePath,
apps,
- getOrigin
+ currentUrl
)
).toEqual({
app: 'bar',
@@ -334,7 +349,7 @@ describe('parseAppUrl', () => {
'https://kibana.local:8080/base-path/custom-bar/path#hash/bang',
basePath,
apps,
- getOrigin
+ currentUrl
)
).toEqual({
app: 'bar',
@@ -345,7 +360,7 @@ describe('parseAppUrl', () => {
'https://kibana.local:8080/base-path/custom-bar/path#hash/bang?hello=dolly',
basePath,
apps,
- getOrigin
+ currentUrl
)
).toEqual({
app: 'bar',
@@ -358,11 +373,11 @@ describe('parseAppUrl', () => {
'https://kibana.local:8080/base-path/app/non-registered',
basePath,
apps,
- getOrigin
+ currentUrl
)
).toEqual(undefined);
expect(
- parseAppUrl('https://kibana.local:8080/base-path/unknown-path', basePath, apps, getOrigin)
+ parseAppUrl('https://kibana.local:8080/base-path/unknown-path', basePath, apps, currentUrl)
).toEqual(undefined);
});
it('returns undefined when origin does not match', () => {
@@ -371,7 +386,7 @@ describe('parseAppUrl', () => {
'https://other-kibana.external:8080/base-path/app/foo',
basePath,
apps,
- getOrigin
+ currentUrl
)
).toEqual(undefined);
expect(
@@ -379,62 +394,14 @@ describe('parseAppUrl', () => {
'https://other-kibana.external:8080/base-path/custom-bar',
basePath,
apps,
- getOrigin
+ currentUrl
)
).toEqual(undefined);
});
- });
-});
-
-describe('getAppInfo', () => {
- const createApp = (props: Partial = {}): App => ({
- mount: () => () => undefined,
- updater$: of(() => undefined),
- id: 'some-id',
- title: 'some-title',
- status: AppStatus.accessible,
- navLinkStatus: AppNavLinkStatus.default,
- appRoute: `/app/some-id`,
- ...props,
- });
-
- it('converts an application and remove sensitive properties', () => {
- const app = createApp();
- const info = getAppInfo(app);
-
- expect(info).toEqual({
- id: 'some-id',
- title: 'some-title',
- status: AppStatus.accessible,
- navLinkStatus: AppNavLinkStatus.visible,
- appRoute: `/app/some-id`,
+ it('returns undefined when the path does not contain the base path', () => {
+ expect(parseAppUrl('https://kibana.local:8080/app/foo', basePath, apps, currentUrl)).toEqual(
+ undefined
+ );
});
});
-
- it('computes the navLinkStatus depending on the app status', () => {
- expect(
- getAppInfo(
- createApp({
- navLinkStatus: AppNavLinkStatus.default,
- status: AppStatus.inaccessible,
- })
- )
- ).toEqual(
- expect.objectContaining({
- navLinkStatus: AppNavLinkStatus.hidden,
- })
- );
- expect(
- getAppInfo(
- createApp({
- navLinkStatus: AppNavLinkStatus.default,
- status: AppStatus.accessible,
- })
- )
- ).toEqual(
- expect.objectContaining({
- navLinkStatus: AppNavLinkStatus.visible,
- })
- );
- });
});
diff --git a/src/core/public/application/utils/parse_app_url.ts b/src/core/public/application/utils/parse_app_url.ts
new file mode 100644
index 0000000000000..d253129a63ae4
--- /dev/null
+++ b/src/core/public/application/utils/parse_app_url.ts
@@ -0,0 +1,83 @@
+/*
+ * 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 { getUrlOrigin } from '@kbn/std';
+import { resolve } from 'url';
+import { IBasePath } from '../../http';
+import { App, ParsedAppUrl } from '../types';
+
+/**
+ * Parse given URL and return the associated app id and path if any app matches, or undefined if none do.
+ * Input can either be:
+ *
+ * - an absolute path containing the basePath,
+ * e.g `/base-path/app/my-app/some-path`
+ *
+ * - an absolute URL matching the `origin` of the Kibana instance (as seen by the browser),
+ * e.g `https://kibana:8080/base-path/app/my-app/some-path`
+ *
+ * - a path relative to the provided `currentUrl`.
+ * e.g with `currentUrl` being `https://kibana:8080/base-path/app/current-app/some-path`
+ * `../other-app/other-path` will be converted to `/base-path/app/other-app/other-path`
+ */
+export const parseAppUrl = (
+ url: string,
+ basePath: IBasePath,
+ apps: Map>,
+ currentUrl: string = window.location.href
+): ParsedAppUrl | undefined => {
+ const currentOrigin = getUrlOrigin(currentUrl);
+ if (!currentOrigin) {
+ throw new Error('when manually provided, currentUrl must be valid url with an origin');
+ }
+ const currentPath = currentUrl.substring(currentOrigin.length);
+
+ // remove the origin from the given url
+ if (url.startsWith(currentOrigin)) {
+ url = url.substring(currentOrigin.length);
+ }
+
+ // if the path is relative (i.e `../../to/somewhere`), we convert it to absolute
+ if (!url.startsWith('/')) {
+ url = resolve(currentPath, url);
+ }
+
+ // if using a basePath and the absolute path does not starts with it, it can't be a match
+ const basePathValue = basePath.get();
+ if (basePathValue && !url.startsWith(basePathValue)) {
+ return undefined;
+ }
+
+ url = basePath.remove(url);
+ if (!url.startsWith('/')) {
+ return undefined;
+ }
+
+ for (const app of apps.values()) {
+ const appPath = app.appRoute || `/app/${app.id}`;
+
+ if (url.startsWith(appPath)) {
+ const path = url.substr(appPath.length);
+ return {
+ app: app.id,
+ path: path.length ? path : undefined,
+ };
+ }
+ }
+};
diff --git a/src/core/public/application/utils/relative_to_absolute.test.ts b/src/core/public/application/utils/relative_to_absolute.test.ts
new file mode 100644
index 0000000000000..56a33450ce902
--- /dev/null
+++ b/src/core/public/application/utils/relative_to_absolute.test.ts
@@ -0,0 +1,29 @@
+/*
+ * 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 { relativeToAbsolute } from './relative_to_absolute';
+
+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`);
+ });
+});
diff --git a/src/core/public/application/utils/relative_to_absolute.ts b/src/core/public/application/utils/relative_to_absolute.ts
new file mode 100644
index 0000000000000..0f24f754f56cd
--- /dev/null
+++ b/src/core/public/application/utils/relative_to_absolute.ts
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+
+/**
+ * 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 `` 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;
+};
diff --git a/src/core/public/application/utils/remove_slashes.test.ts b/src/core/public/application/utils/remove_slashes.test.ts
new file mode 100644
index 0000000000000..719e1ea08d109
--- /dev/null
+++ b/src/core/public/application/utils/remove_slashes.test.ts
@@ -0,0 +1,53 @@
+/*
+ * 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 { removeSlashes } from './remove_slashes';
+
+describe('removeSlashes', () => {
+ it('only removes duplicates by default', () => {
+ expect(removeSlashes('/some//url//to//')).toEqual('/some/url/to/');
+ expect(removeSlashes('some/////other//url')).toEqual('some/other/url');
+ });
+
+ it('remove trailing slash when `trailing` is true', () => {
+ expect(removeSlashes('/some//url//to//', { trailing: true })).toEqual('/some/url/to');
+ });
+
+ it('remove leading slash when `leading` is true', () => {
+ expect(removeSlashes('/some//url//to//', { leading: true })).toEqual('some/url/to/');
+ });
+
+ it('does not removes duplicates when `duplicates` is false', () => {
+ expect(removeSlashes('/some//url//to/', { leading: true, duplicates: false })).toEqual(
+ 'some//url//to/'
+ );
+ expect(removeSlashes('/some//url//to/', { trailing: true, duplicates: false })).toEqual(
+ '/some//url//to'
+ );
+ });
+
+ it('accept mixed options', () => {
+ expect(
+ removeSlashes('/some//url//to/', { leading: true, duplicates: false, trailing: true })
+ ).toEqual('some//url//to');
+ expect(
+ removeSlashes('/some//url//to/', { leading: true, duplicates: true, trailing: true })
+ ).toEqual('some/url/to');
+ });
+});
diff --git a/src/core/public/application/utils/remove_slashes.ts b/src/core/public/application/utils/remove_slashes.ts
new file mode 100644
index 0000000000000..641d7bc4164f4
--- /dev/null
+++ b/src/core/public/application/utils/remove_slashes.ts
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+/**
+ * Utility to remove trailing, leading or duplicate slashes.
+ * By default will only remove duplicates.
+ */
+export const removeSlashes = (
+ url: string,
+ {
+ trailing = false,
+ leading = false,
+ duplicates = true,
+ }: { trailing?: boolean; leading?: boolean; duplicates?: boolean } = {}
+): string => {
+ if (duplicates) {
+ url = url.replace(/\/{2,}/g, '/');
+ }
+ if (trailing) {
+ url = url.replace(/\/$/, '');
+ }
+ if (leading) {
+ url = url.replace(/^\//, '');
+ }
+ return url;
+};
diff --git a/src/core/public/http/base_path.ts b/src/core/public/http/base_path.ts
index 5d9eb51023b78..78e9cf75ff806 100644
--- a/src/core/public/http/base_path.ts
+++ b/src/core/public/http/base_path.ts
@@ -16,24 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
-/*
- * 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 { modifyUrl } from '@kbn/std';