Skip to content

Commit cf43f13

Browse files
Merge branch '7.x' into backport/7.x/pr-63874
2 parents 2bf536e + e5946ce commit cf43f13

File tree

52 files changed

+1204
-291
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1204
-291
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
2+
3+
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [AppBase](./kibana-plugin-core-public.appbase.md) &gt; [defaultPath](./kibana-plugin-core-public.appbase.defaultpath.md)
4+
5+
## AppBase.defaultPath property
6+
7+
Allow to define the default path a user should be directed to when navigating to the app. When defined, this value will be used as a default for the `path` option when calling [navigateToApp](./kibana-plugin-core-public.applicationstart.navigatetoapp.md)<!-- -->\`<!-- -->, and will also be appended to the [application navLink](./kibana-plugin-core-public.chromenavlink.md) in the navigation bar.
8+
9+
<b>Signature:</b>
10+
11+
```typescript
12+
defaultPath?: string;
13+
```

docs/development/core/public/kibana-plugin-core-public.appbase.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface AppBase
1818
| [capabilities](./kibana-plugin-core-public.appbase.capabilities.md) | <code>Partial&lt;Capabilities&gt;</code> | Custom capabilities defined by the app. |
1919
| [category](./kibana-plugin-core-public.appbase.category.md) | <code>AppCategory</code> | The category definition of the product See [AppCategory](./kibana-plugin-core-public.appcategory.md) See DEFAULT\_APP\_CATEGORIES for more reference |
2020
| [chromeless](./kibana-plugin-core-public.appbase.chromeless.md) | <code>boolean</code> | Hide the UI chrome when the application is mounted. Defaults to <code>false</code>. Takes precedence over chrome service visibility settings. |
21+
| [defaultPath](./kibana-plugin-core-public.appbase.defaultpath.md) | <code>string</code> | Allow to define the default path a user should be directed to when navigating to the app. When defined, this value will be used as a default for the <code>path</code> option when calling [navigateToApp](./kibana-plugin-core-public.applicationstart.navigatetoapp.md)<!-- -->\`<!-- -->, and will also be appended to the [application navLink](./kibana-plugin-core-public.chromenavlink.md) in the navigation bar. |
2122
| [euiIconType](./kibana-plugin-core-public.appbase.euiicontype.md) | <code>string</code> | A EUI iconType that will be used for the app's icon. This icon takes precendence over the <code>icon</code> property. |
2223
| [icon](./kibana-plugin-core-public.appbase.icon.md) | <code>string</code> | A URL to an image file used as an icon. Used as a fallback if <code>euiIconType</code> is not provided. |
2324
| [id](./kibana-plugin-core-public.appbase.id.md) | <code>string</code> | The unique identifier of the application |

docs/development/core/public/kibana-plugin-core-public.appupdatablefields.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ Defines the list of fields that can be updated via an [AppUpdater](./kibana-plug
99
<b>Signature:</b>
1010

1111
```typescript
12-
export declare type AppUpdatableFields = Pick<AppBase, 'status' | 'navLinkStatus' | 'tooltip'>;
12+
export declare type AppUpdatableFields = Pick<AppBase, 'status' | 'navLinkStatus' | 'tooltip' | 'defaultPath'>;
1313
```

docs/development/core/public/kibana-plugin-core-public.chromenavlink.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,5 @@ export interface ChromeNavLink
2929
| [subUrlBase](./kibana-plugin-core-public.chromenavlink.suburlbase.md) | <code>string</code> | A url base that legacy apps can set to match deep URLs to an application. |
3030
| [title](./kibana-plugin-core-public.chromenavlink.title.md) | <code>string</code> | The title of the application. |
3131
| [tooltip](./kibana-plugin-core-public.chromenavlink.tooltip.md) | <code>string</code> | A tooltip shown when hovering over an app link. |
32-
| [url](./kibana-plugin-core-public.chromenavlink.url.md) | <code>string</code> | A url that legacy apps can set to deep link into their applications. |
32+
| [url](./kibana-plugin-core-public.chromenavlink.url.md) | <code>string</code> | The route used to open the [default path](./kibana-plugin-core-public.appbase.defaultpath.md) of an application. If unset, <code>baseUrl</code> will be used instead. |
3333

docs/development/core/public/kibana-plugin-core-public.chromenavlink.url.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,7 @@
44

55
## ChromeNavLink.url property
66

7-
> Warning: This API is now obsolete.
8-
>
9-
>
10-
11-
A url that legacy apps can set to deep link into their applications.
7+
The route used to open the [default path](./kibana-plugin-core-public.appbase.defaultpath.md) of an application. If unset, `baseUrl` will be used instead.
128

139
<b>Signature:</b>
1410

src/core/public/application/application_service.test.ts

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ describe('#setup()', () => {
8787
).toThrowErrorMatchingInlineSnapshot(`"Applications cannot be registered after \\"setup\\""`);
8888
});
8989

90-
it('allows to register a statusUpdater for the application', async () => {
90+
it('allows to register an AppUpdater for the application', async () => {
9191
const setup = service.setup(setupDeps);
9292

9393
const pluginId = Symbol('plugin');
@@ -118,6 +118,7 @@ describe('#setup()', () => {
118118
updater$.next(app => ({
119119
status: AppStatus.inaccessible,
120120
tooltip: 'App inaccessible due to reason',
121+
defaultPath: 'foo/bar',
121122
}));
122123

123124
applications = await applications$.pipe(take(1)).toPromise();
@@ -128,6 +129,7 @@ describe('#setup()', () => {
128129
legacy: false,
129130
navLinkStatus: AppNavLinkStatus.default,
130131
status: AppStatus.inaccessible,
132+
defaultPath: 'foo/bar',
131133
tooltip: 'App inaccessible due to reason',
132134
})
133135
);
@@ -209,7 +211,7 @@ describe('#setup()', () => {
209211
});
210212
});
211213

212-
describe('registerAppStatusUpdater', () => {
214+
describe('registerAppUpdater', () => {
213215
it('updates status fields', async () => {
214216
const setup = service.setup(setupDeps);
215217

@@ -413,6 +415,36 @@ describe('#setup()', () => {
413415
})
414416
);
415417
});
418+
419+
it('allows to update the basePath', async () => {
420+
const setup = service.setup(setupDeps);
421+
422+
const pluginId = Symbol('plugin');
423+
setup.register(pluginId, createApp({ id: 'app1' }));
424+
425+
const updater = new BehaviorSubject<AppUpdater>(app => ({}));
426+
setup.registerAppUpdater(updater);
427+
428+
const start = await service.start(startDeps);
429+
await start.navigateToApp('app1');
430+
expect(MockHistory.push).toHaveBeenCalledWith('/app/app1', undefined);
431+
MockHistory.push.mockClear();
432+
433+
updater.next(app => ({ defaultPath: 'default-path' }));
434+
await start.navigateToApp('app1');
435+
expect(MockHistory.push).toHaveBeenCalledWith('/app/app1/default-path', undefined);
436+
MockHistory.push.mockClear();
437+
438+
updater.next(app => ({ defaultPath: 'another-path' }));
439+
await start.navigateToApp('app1');
440+
expect(MockHistory.push).toHaveBeenCalledWith('/app/app1/another-path', undefined);
441+
MockHistory.push.mockClear();
442+
443+
updater.next(app => ({}));
444+
await start.navigateToApp('app1');
445+
expect(MockHistory.push).toHaveBeenCalledWith('/app/app1', undefined);
446+
MockHistory.push.mockClear();
447+
});
416448
});
417449

418450
it("`registerMountContext` calls context container's registerContext", () => {
@@ -676,6 +708,57 @@ describe('#start()', () => {
676708
expect(MockHistory.push).toHaveBeenCalledWith('/custom/path#/hash/router/path', undefined);
677709
});
678710

711+
it('preserves trailing slash when path contains a hash', async () => {
712+
const { register } = service.setup(setupDeps);
713+
714+
register(Symbol(), createApp({ id: 'app2', appRoute: '/custom/app-path' }));
715+
716+
const { navigateToApp } = await service.start(startDeps);
717+
await navigateToApp('app2', { path: '#/' });
718+
expect(MockHistory.push).toHaveBeenCalledWith('/custom/app-path#/', undefined);
719+
MockHistory.push.mockClear();
720+
721+
await navigateToApp('app2', { path: '#/foo/bar/' });
722+
expect(MockHistory.push).toHaveBeenCalledWith('/custom/app-path#/foo/bar/', undefined);
723+
MockHistory.push.mockClear();
724+
725+
await navigateToApp('app2', { path: '/path#/' });
726+
expect(MockHistory.push).toHaveBeenCalledWith('/custom/app-path/path#/', undefined);
727+
MockHistory.push.mockClear();
728+
729+
await navigateToApp('app2', { path: '/path#/hash/' });
730+
expect(MockHistory.push).toHaveBeenCalledWith('/custom/app-path/path#/hash/', undefined);
731+
MockHistory.push.mockClear();
732+
733+
await navigateToApp('app2', { path: '/path/' });
734+
expect(MockHistory.push).toHaveBeenCalledWith('/custom/app-path/path', undefined);
735+
MockHistory.push.mockClear();
736+
});
737+
738+
it('appends the defaultPath when the path parameter is not specified', async () => {
739+
const { register } = service.setup(setupDeps);
740+
741+
register(Symbol(), createApp({ id: 'app1', defaultPath: 'default/path' }));
742+
register(
743+
Symbol(),
744+
createApp({ id: 'app2', appRoute: '/custom-app-path', defaultPath: '/my-base' })
745+
);
746+
747+
const { navigateToApp } = await service.start(startDeps);
748+
749+
await navigateToApp('app1', { path: 'defined-path' });
750+
expect(MockHistory.push).toHaveBeenCalledWith('/app/app1/defined-path', undefined);
751+
752+
await navigateToApp('app1', {});
753+
expect(MockHistory.push).toHaveBeenCalledWith('/app/app1/default/path', undefined);
754+
755+
await navigateToApp('app2', { path: 'defined-path' });
756+
expect(MockHistory.push).toHaveBeenCalledWith('/custom-app-path/defined-path', undefined);
757+
758+
await navigateToApp('app2', {});
759+
expect(MockHistory.push).toHaveBeenCalledWith('/custom-app-path/my-base', undefined);
760+
});
761+
679762
it('includes state if specified', async () => {
680763
const { register } = service.setup(setupDeps);
681764

src/core/public/application/application_service.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
Mounter,
4747
} from './types';
4848
import { getLeaveAction, isConfirmAction } from './application_leave';
49+
import { appendAppPath } from './utils';
4950

5051
interface SetupDeps {
5152
context: ContextSetup;
@@ -81,13 +82,7 @@ const getAppUrl = (mounters: Map<string, Mounter>, appId: string, path: string =
8182
const appBasePath = mounters.get(appId)?.appRoute
8283
? `/${mounters.get(appId)!.appRoute}`
8384
: `/app/${appId}`;
84-
85-
// Only preppend slash if not a hash or query path
86-
path = path.startsWith('#') || path.startsWith('?') ? path : `/${path}`;
87-
88-
return `${appBasePath}${path}`
89-
.replace(/\/{2,}/g, '/') // Remove duplicate slashes
90-
.replace(/\/$/, ''); // Remove trailing slash
85+
return appendAppPath(appBasePath, path);
9186
};
9287

9388
const allApplicationsFilter = '__ALL__';
@@ -290,6 +285,9 @@ export class ApplicationService {
290285
},
291286
navigateToApp: async (appId, { path, state }: { path?: string; state?: any } = {}) => {
292287
if (await this.shouldNavigate(overlays)) {
288+
if (path === undefined) {
289+
path = applications$.value.get(appId)?.defaultPath;
290+
}
293291
this.appLeaveHandlers.delete(this.currentAppId$.value!);
294292
this.navigate!(getAppUrl(availableMounters, appId, path), state);
295293
this.currentAppId$.next(appId);

src/core/public/application/types.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@ export interface AppBase {
6666
*/
6767
navLinkStatus?: AppNavLinkStatus;
6868

69+
/**
70+
* Allow to define the default path a user should be directed to when navigating to the app.
71+
* When defined, this value will be used as a default for the `path` option when calling {@link ApplicationStart.navigateToApp | navigateToApp}`,
72+
* and will also be appended to the {@link ChromeNavLink | application navLink} in the navigation bar.
73+
*/
74+
defaultPath?: string;
75+
6976
/**
7077
* An {@link AppUpdater} observable that can be used to update the application {@link AppUpdatableFields} at runtime.
7178
*
@@ -187,7 +194,10 @@ export enum AppNavLinkStatus {
187194
* Defines the list of fields that can be updated via an {@link AppUpdater}.
188195
* @public
189196
*/
190-
export type AppUpdatableFields = Pick<AppBase, 'status' | 'navLinkStatus' | 'tooltip'>;
197+
export type AppUpdatableFields = Pick<
198+
AppBase,
199+
'status' | 'navLinkStatus' | 'tooltip' | 'defaultPath'
200+
>;
191201

192202
/**
193203
* Updater for applications.
@@ -642,7 +652,8 @@ export interface ApplicationStart {
642652
* Navigate to a given app
643653
*
644654
* @param appId
645-
* @param options.path - optional path inside application to deep link to
655+
* @param options.path - optional path inside application to deep link to.
656+
* If undefined, will use {@link AppBase.defaultPath | the app's default path}` as default.
646657
* @param options.state - optional state to forward to the application
647658
*/
648659
navigateToApp(appId: string, options?: { path?: string; state?: any }): Promise<void>;
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import { removeSlashes, appendAppPath } from './utils';
21+
22+
describe('removeSlashes', () => {
23+
it('only removes duplicates by default', () => {
24+
expect(removeSlashes('/some//url//to//')).toEqual('/some/url/to/');
25+
expect(removeSlashes('some/////other//url')).toEqual('some/other/url');
26+
});
27+
28+
it('remove trailing slash when `trailing` is true', () => {
29+
expect(removeSlashes('/some//url//to//', { trailing: true })).toEqual('/some/url/to');
30+
});
31+
32+
it('remove leading slash when `leading` is true', () => {
33+
expect(removeSlashes('/some//url//to//', { leading: true })).toEqual('some/url/to/');
34+
});
35+
36+
it('does not removes duplicates when `duplicates` is false', () => {
37+
expect(removeSlashes('/some//url//to/', { leading: true, duplicates: false })).toEqual(
38+
'some//url//to/'
39+
);
40+
expect(removeSlashes('/some//url//to/', { trailing: true, duplicates: false })).toEqual(
41+
'/some//url//to'
42+
);
43+
});
44+
45+
it('accept mixed options', () => {
46+
expect(
47+
removeSlashes('/some//url//to/', { leading: true, duplicates: false, trailing: true })
48+
).toEqual('some//url//to');
49+
expect(
50+
removeSlashes('/some//url//to/', { leading: true, duplicates: true, trailing: true })
51+
).toEqual('some/url/to');
52+
});
53+
});
54+
55+
describe('appendAppPath', () => {
56+
it('appends the appBasePath with given path', () => {
57+
expect(appendAppPath('/app/my-app', '/some-path')).toEqual('/app/my-app/some-path');
58+
expect(appendAppPath('/app/my-app/', 'some-path')).toEqual('/app/my-app/some-path');
59+
expect(appendAppPath('/app/my-app', 'some-path')).toEqual('/app/my-app/some-path');
60+
expect(appendAppPath('/app/my-app', '')).toEqual('/app/my-app');
61+
});
62+
63+
it('preserves the trailing slash only if included in the hash', () => {
64+
expect(appendAppPath('/app/my-app', '/some-path/')).toEqual('/app/my-app/some-path');
65+
expect(appendAppPath('/app/my-app', '/some-path#/')).toEqual('/app/my-app/some-path#/');
66+
expect(appendAppPath('/app/my-app', '/some-path#/hash/')).toEqual(
67+
'/app/my-app/some-path#/hash/'
68+
);
69+
expect(appendAppPath('/app/my-app', '/some-path#/hash')).toEqual('/app/my-app/some-path#/hash');
70+
});
71+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
/**
21+
* Utility to remove trailing, leading or duplicate slashes.
22+
* By default will only remove duplicates.
23+
*/
24+
export const removeSlashes = (
25+
url: string,
26+
{
27+
trailing = false,
28+
leading = false,
29+
duplicates = true,
30+
}: { trailing?: boolean; leading?: boolean; duplicates?: boolean } = {}
31+
): string => {
32+
if (duplicates) {
33+
url = url.replace(/\/{2,}/g, '/');
34+
}
35+
if (trailing) {
36+
url = url.replace(/\/$/, '');
37+
}
38+
if (leading) {
39+
url = url.replace(/^\//, '');
40+
}
41+
return url;
42+
};
43+
44+
export const appendAppPath = (appBasePath: string, path: string = '') => {
45+
// Only prepend slash if not a hash or query path
46+
path = path === '' || path.startsWith('#') || path.startsWith('?') ? path : `/${path}`;
47+
// Do not remove trailing slash when in hashbang
48+
const removeTrailing = path.indexOf('#') === -1;
49+
return removeSlashes(`${appBasePath}${path}`, {
50+
trailing: removeTrailing,
51+
duplicates: true,
52+
leading: false,
53+
});
54+
};

0 commit comments

Comments
 (0)