mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Core][Navigation] Chrome nav display application deepLinks (#100590)
* chrome nav allows deepLinks * docs updated * use ChromeNavLink.url to call navigateToUrl * to_nav_link test cases added for deepLink parameter * snapshots updated * deep nav links functional test added * AppNavOptions type encapsulation * docs updated * docs for AppNavOptions * implement navigateToApp deepLinkId option * app searchable flag implementation * code cleaning and test case added * use explicit type Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
15ab9d70fd
commit
b740640a2a
50 changed files with 1216 additions and 145 deletions
|
@ -8,7 +8,7 @@
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface App<HistoryLocationState = unknown>
|
||||
export interface App<HistoryLocationState = unknown> extends AppNavOptions
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
@ -21,16 +21,13 @@ export interface App<HistoryLocationState = unknown>
|
|||
| [chromeless](./kibana-plugin-core-public.app.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. |
|
||||
| [deepLinks](./kibana-plugin-core-public.app.deeplinks.md) | <code>AppDeepLink[]</code> | Input type for registering secondary in-app locations for an application.<!-- -->Deep links must include at least one of <code>path</code> or <code>deepLinks</code>. A deep link that does not have a <code>path</code> represents a topological level in the application's hierarchy, but does not have a destination URL that is user-accessible. |
|
||||
| [defaultPath](./kibana-plugin-core-public.app.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. |
|
||||
| [euiIconType](./kibana-plugin-core-public.app.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. |
|
||||
| [exactRoute](./kibana-plugin-core-public.app.exactroute.md) | <code>boolean</code> | If set to true, the application's route will only be checked against an exact match. Defaults to <code>false</code>. |
|
||||
| [icon](./kibana-plugin-core-public.app.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. |
|
||||
| [id](./kibana-plugin-core-public.app.id.md) | <code>string</code> | The unique identifier of the application |
|
||||
| [keywords](./kibana-plugin-core-public.app.keywords.md) | <code>string[]</code> | Optional keywords to match with in deep links search. Omit if this part of the hierarchy does not have a page URL. |
|
||||
| [mount](./kibana-plugin-core-public.app.mount.md) | <code>AppMount<HistoryLocationState></code> | A mount function called when the user navigates to this app's route. |
|
||||
| [navLinkStatus](./kibana-plugin-core-public.app.navlinkstatus.md) | <code>AppNavLinkStatus</code> | The initial status of the application's navLink. Defaulting to <code>visible</code> if <code>status</code> is <code>accessible</code> and <code>hidden</code> if status is <code>inaccessible</code> See [AppNavLinkStatus](./kibana-plugin-core-public.appnavlinkstatus.md) |
|
||||
| [order](./kibana-plugin-core-public.app.order.md) | <code>number</code> | An ordinal used to sort nav links relative to one another for display. |
|
||||
| [searchable](./kibana-plugin-core-public.app.searchable.md) | <code>boolean</code> | The initial flag to determine if the application is searchable in the global search. Defaulting to <code>true</code> if <code>navLinkStatus</code> is <code>visible</code> or omitted. |
|
||||
| [status](./kibana-plugin-core-public.app.status.md) | <code>AppStatus</code> | The initial status of the application. Defaulting to <code>accessible</code> |
|
||||
| [title](./kibana-plugin-core-public.app.title.md) | <code>string</code> | The title of the application. |
|
||||
| [tooltip](./kibana-plugin-core-public.app.tooltip.md) | <code>string</code> | A tooltip shown when hovering over app link. |
|
||||
| [updater$](./kibana-plugin-core-public.app.updater_.md) | <code>Observable<AppUpdater></code> | An [AppUpdater](./kibana-plugin-core-public.appupdater.md) observable that can be used to update the application [AppUpdatableFields](./kibana-plugin-core-public.appupdatablefields.md) at runtime. |
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [searchable](./kibana-plugin-core-public.app.searchable.md)
|
||||
|
||||
## App.searchable property
|
||||
|
||||
The initial flag to determine if the application is searchable in the global search. Defaulting to `true` if `navLinkStatus` is `visible` or omitted.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
searchable?: boolean;
|
||||
```
|
|
@ -16,7 +16,8 @@ export declare type AppDeepLink = {
|
|||
title: string;
|
||||
keywords?: string[];
|
||||
navLinkStatus?: AppNavLinkStatus;
|
||||
} & ({
|
||||
searchable?: boolean;
|
||||
} & AppNavOptions & ({
|
||||
path: string;
|
||||
deepLinks?: AppDeepLink[];
|
||||
} | {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [euiIconType](./kibana-plugin-core-public.app.euiicontype.md)
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppNavOptions](./kibana-plugin-core-public.appnavoptions.md) > [euiIconType](./kibana-plugin-core-public.appnavoptions.euiicontype.md)
|
||||
|
||||
## App.euiIconType property
|
||||
## AppNavOptions.euiIconType property
|
||||
|
||||
A EUI iconType that will be used for the app's icon. This icon takes precendence over the `icon` property.
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [icon](./kibana-plugin-core-public.app.icon.md)
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppNavOptions](./kibana-plugin-core-public.appnavoptions.md) > [icon](./kibana-plugin-core-public.appnavoptions.icon.md)
|
||||
|
||||
## App.icon property
|
||||
## AppNavOptions.icon property
|
||||
|
||||
A URL to an image file used as an icon. Used as a fallback if `euiIconType` is not provided.
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppNavOptions](./kibana-plugin-core-public.appnavoptions.md)
|
||||
|
||||
## AppNavOptions interface
|
||||
|
||||
App navigation menu options
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export interface AppNavOptions
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [euiIconType](./kibana-plugin-core-public.appnavoptions.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. |
|
||||
| [icon](./kibana-plugin-core-public.appnavoptions.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. |
|
||||
| [order](./kibana-plugin-core-public.appnavoptions.order.md) | <code>number</code> | An ordinal used to sort nav links relative to one another for display. |
|
||||
| [tooltip](./kibana-plugin-core-public.appnavoptions.tooltip.md) | <code>string</code> | A tooltip shown when hovering over app link. |
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [order](./kibana-plugin-core-public.app.order.md)
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppNavOptions](./kibana-plugin-core-public.appnavoptions.md) > [order](./kibana-plugin-core-public.appnavoptions.order.md)
|
||||
|
||||
## App.order property
|
||||
## AppNavOptions.order property
|
||||
|
||||
An ordinal used to sort nav links relative to one another for display.
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [App](./kibana-plugin-core-public.app.md) > [tooltip](./kibana-plugin-core-public.app.tooltip.md)
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppNavOptions](./kibana-plugin-core-public.appnavoptions.md) > [tooltip](./kibana-plugin-core-public.appnavoptions.tooltip.md)
|
||||
|
||||
## App.tooltip property
|
||||
## AppNavOptions.tooltip property
|
||||
|
||||
A tooltip shown when hovering over app link.
|
||||
|
|
@ -9,5 +9,5 @@ Defines the list of fields that can be updated via an [AppUpdater](./kibana-plug
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type AppUpdatableFields = Pick<App, 'status' | 'navLinkStatus' | 'tooltip' | 'defaultPath' | 'deepLinks'>;
|
||||
export declare type AppUpdatableFields = Pick<App, 'status' | 'navLinkStatus' | 'searchable' | 'tooltip' | 'defaultPath' | 'deepLinks'>;
|
||||
```
|
||||
|
|
|
@ -26,5 +26,5 @@ export interface ChromeNavLink
|
|||
| [order](./kibana-plugin-core-public.chromenavlink.order.md) | <code>number</code> | An ordinal used to sort nav links relative to one another for display. |
|
||||
| [title](./kibana-plugin-core-public.chromenavlink.title.md) | <code>string</code> | The title of the application. |
|
||||
| [tooltip](./kibana-plugin-core-public.chromenavlink.tooltip.md) | <code>string</code> | A tooltip shown when hovering over an app link. |
|
||||
| [url](./kibana-plugin-core-public.chromenavlink.url.md) | <code>string</code> | The route used to open the of an application. If unset, <code>baseUrl</code> will be used instead. |
|
||||
| [url](./kibana-plugin-core-public.chromenavlink.url.md) | <code>string</code> | The route used to open the default path and the deep links of an application. |
|
||||
|
||||
|
|
|
@ -4,10 +4,10 @@
|
|||
|
||||
## ChromeNavLink.url property
|
||||
|
||||
The route used to open the of an application. If unset, `baseUrl` will be used instead.
|
||||
The route used to open the default path and the deep links of an application.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
readonly url?: string;
|
||||
readonly url: string;
|
||||
```
|
||||
|
|
|
@ -38,6 +38,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
|
|||
| [ApplicationSetup](./kibana-plugin-core-public.applicationsetup.md) | |
|
||||
| [ApplicationStart](./kibana-plugin-core-public.applicationstart.md) | |
|
||||
| [AppMountParameters](./kibana-plugin-core-public.appmountparameters.md) | |
|
||||
| [AppNavOptions](./kibana-plugin-core-public.appnavoptions.md) | App navigation menu options |
|
||||
| [AsyncPlugin](./kibana-plugin-core-public.asyncplugin.md) | A plugin with asynchronous lifecycle methods. |
|
||||
| [Capabilities](./kibana-plugin-core-public.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. |
|
||||
| [ChromeBadge](./kibana-plugin-core-public.chromebadge.md) | |
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [NavigateToAppOptions](./kibana-plugin-core-public.navigatetoappoptions.md) > [deepLinkId](./kibana-plugin-core-public.navigatetoappoptions.deeplinkid.md)
|
||||
|
||||
## NavigateToAppOptions.deepLinkId property
|
||||
|
||||
optional [deep link](./kibana-plugin-core-public.app.deeplinks.md) id inside the application to navigate to. If an additional [path](./kibana-plugin-core-public.navigatetoappoptions.path.md) is defined it will be appended to the deep link path.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
deepLinkId?: string;
|
||||
```
|
|
@ -16,8 +16,9 @@ export interface NavigateToAppOptions
|
|||
|
||||
| Property | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| [deepLinkId](./kibana-plugin-core-public.navigatetoappoptions.deeplinkid.md) | <code>string</code> | optional [deep link](./kibana-plugin-core-public.app.deeplinks.md) id inside the application to navigate to. If an additional [path](./kibana-plugin-core-public.navigatetoappoptions.path.md) is defined it will be appended to the deep link path. |
|
||||
| [openInNewTab](./kibana-plugin-core-public.navigatetoappoptions.openinnewtab.md) | <code>boolean</code> | if true, will open the app in new tab, will share session information via window.open if base |
|
||||
| [path](./kibana-plugin-core-public.navigatetoappoptions.path.md) | <code>string</code> | optional path inside application to deep link to. If undefined, will use [the app's default path](./kibana-plugin-core-public.app.defaultpath.md)<!-- -->\` as default. |
|
||||
| [path](./kibana-plugin-core-public.navigatetoappoptions.path.md) | <code>string</code> | optional path inside application to deep link to. If undefined, will use [the app's default path](./kibana-plugin-core-public.app.defaultpath.md) as default. |
|
||||
| [replace](./kibana-plugin-core-public.navigatetoappoptions.replace.md) | <code>boolean</code> | if true, will not create a new history entry when navigating (using <code>replace</code> instead of <code>push</code>) |
|
||||
| [state](./kibana-plugin-core-public.navigatetoappoptions.state.md) | <code>unknown</code> | optional state to forward to the application |
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
## NavigateToAppOptions.path property
|
||||
|
||||
optional path inside application to deep link to. If undefined, will use [the app's default path](./kibana-plugin-core-public.app.defaultpath.md)<!-- -->\` as default.
|
||||
optional path inside application to deep link to. If undefined, will use [the app's default path](./kibana-plugin-core-public.app.defaultpath.md) as default.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
|
|
|
@ -9,9 +9,10 @@ Public information about a registered app's [deepLinks](./kibana-plugin-core-pub
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type PublicAppDeepLinkInfo = Omit<AppDeepLink, 'deepLinks' | 'keywords' | 'navLinkStatus'> & {
|
||||
export declare type PublicAppDeepLinkInfo = Omit<AppDeepLink, 'deepLinks' | 'keywords' | 'navLinkStatus' | 'searchable'> & {
|
||||
deepLinks: PublicAppDeepLinkInfo[];
|
||||
keywords: string[];
|
||||
navLinkStatus: AppNavLinkStatus;
|
||||
searchable: boolean;
|
||||
};
|
||||
```
|
||||
|
|
|
@ -9,11 +9,12 @@ Public information about a registered [application](./kibana-plugin-core-public.
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type PublicAppInfo = Omit<App, 'mount' | 'updater$' | 'keywords' | 'deepLinks'> & {
|
||||
export declare type PublicAppInfo = Omit<App, 'mount' | 'updater$' | 'keywords' | 'deepLinks' | 'searchable'> & {
|
||||
status: AppStatus;
|
||||
navLinkStatus: AppNavLinkStatus;
|
||||
appRoute: string;
|
||||
keywords: string[];
|
||||
deepLinks: PublicAppDeepLinkInfo[];
|
||||
searchable: boolean;
|
||||
};
|
||||
```
|
||||
|
|
|
@ -107,6 +107,7 @@ describe('#setup()', () => {
|
|||
status: AppStatus.inaccessible,
|
||||
tooltip: 'App inaccessible due to reason',
|
||||
defaultPath: 'foo/bar',
|
||||
deepLinks: [{ id: 'subapp2', title: 'Subapp 2', path: '/subapp2' }],
|
||||
}));
|
||||
|
||||
applications = await applications$.pipe(take(1)).toPromise();
|
||||
|
@ -118,6 +119,9 @@ describe('#setup()', () => {
|
|||
status: AppStatus.inaccessible,
|
||||
defaultPath: 'foo/bar',
|
||||
tooltip: 'App inaccessible due to reason',
|
||||
deepLinks: [
|
||||
expect.objectContaining({ id: 'subapp2', title: 'Subapp 2', path: '/subapp2' }),
|
||||
],
|
||||
})
|
||||
);
|
||||
expect(applications.get('app2')).toEqual(
|
||||
|
@ -814,6 +818,128 @@ describe('#start()', () => {
|
|||
expect(MockHistory.replace).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deepLinkId option', () => {
|
||||
beforeEach(() => {
|
||||
MockHistory.push.mockClear();
|
||||
});
|
||||
|
||||
it('preserves trailing slash when path contains a hash', async () => {
|
||||
const { register } = service.setup(setupDeps);
|
||||
|
||||
register(
|
||||
Symbol(),
|
||||
createApp({
|
||||
id: 'app1',
|
||||
appRoute: '/custom/app-path',
|
||||
deepLinks: [{ id: 'dl1', title: 'deep link 1', path: '/deep-link' }],
|
||||
})
|
||||
);
|
||||
|
||||
const { navigateToApp } = await service.start(startDeps);
|
||||
await navigateToApp('app1', { deepLinkId: 'dl1', path: '#/' });
|
||||
expect(MockHistory.push).toHaveBeenLastCalledWith(
|
||||
'/custom/app-path/deep-link#/',
|
||||
undefined
|
||||
);
|
||||
|
||||
await navigateToApp('app1', { deepLinkId: 'dl1', path: '#/foo/bar/' });
|
||||
expect(MockHistory.push).toHaveBeenLastCalledWith(
|
||||
'/custom/app-path/deep-link#/foo/bar/',
|
||||
undefined
|
||||
);
|
||||
|
||||
await navigateToApp('app1', { deepLinkId: 'dl1', path: '/path#/' });
|
||||
expect(MockHistory.push).toHaveBeenLastCalledWith(
|
||||
'/custom/app-path/deep-link/path#/',
|
||||
undefined
|
||||
);
|
||||
|
||||
await navigateToApp('app1', { deepLinkId: 'dl1', path: '/path#/hash/' });
|
||||
expect(MockHistory.push).toHaveBeenLastCalledWith(
|
||||
'/custom/app-path/deep-link/path#/hash/',
|
||||
undefined
|
||||
);
|
||||
|
||||
await navigateToApp('app1', { deepLinkId: 'dl1', path: '/path/' });
|
||||
expect(MockHistory.push).toHaveBeenLastCalledWith(
|
||||
'/custom/app-path/deep-link/path',
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
it('omits the defaultPath when the deepLinkId parameter is specified', async () => {
|
||||
const { register } = service.setup(setupDeps);
|
||||
|
||||
register(
|
||||
Symbol(),
|
||||
createApp({
|
||||
id: 'app1',
|
||||
defaultPath: 'default/path',
|
||||
deepLinks: [{ id: 'dl1', title: 'deep link 1', path: '/deep-link' }],
|
||||
})
|
||||
);
|
||||
register(
|
||||
Symbol(),
|
||||
createApp({
|
||||
id: 'app2',
|
||||
appRoute: '/custom-app-path',
|
||||
defaultPath: '/my-default',
|
||||
deepLinks: [{ id: 'dl2', title: 'deep link 2', path: '/deep-link-2' }],
|
||||
})
|
||||
);
|
||||
|
||||
const { navigateToApp } = await service.start(startDeps);
|
||||
|
||||
await navigateToApp('app1', {});
|
||||
expect(MockHistory.push).toHaveBeenLastCalledWith('/app/app1/default/path', undefined);
|
||||
|
||||
await navigateToApp('app1', { deepLinkId: 'dl1' });
|
||||
expect(MockHistory.push).toHaveBeenLastCalledWith('/app/app1/deep-link', undefined);
|
||||
|
||||
await navigateToApp('app1', { deepLinkId: 'dl1', path: 'some-other-path' });
|
||||
expect(MockHistory.push).toHaveBeenLastCalledWith(
|
||||
'/app/app1/deep-link/some-other-path',
|
||||
undefined
|
||||
);
|
||||
|
||||
await navigateToApp('app2', {});
|
||||
expect(MockHistory.push).toHaveBeenLastCalledWith('/custom-app-path/my-default', undefined);
|
||||
|
||||
await navigateToApp('app2', { deepLinkId: 'dl2' });
|
||||
expect(MockHistory.push).toHaveBeenLastCalledWith(
|
||||
'/custom-app-path/deep-link-2',
|
||||
undefined
|
||||
);
|
||||
|
||||
await navigateToApp('app2', { deepLinkId: 'dl2', path: 'some-other-path' });
|
||||
expect(MockHistory.push).toHaveBeenLastCalledWith(
|
||||
'/custom-app-path/deep-link-2/some-other-path',
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
it('ignores the deepLinkId parameter if it is unknown', async () => {
|
||||
const { register } = service.setup(setupDeps);
|
||||
|
||||
register(
|
||||
Symbol(),
|
||||
createApp({
|
||||
id: 'app1',
|
||||
defaultPath: 'default/path',
|
||||
deepLinks: [{ id: 'dl1', title: 'deep link 1', path: '/deep-link' }],
|
||||
})
|
||||
);
|
||||
|
||||
const { navigateToApp } = await service.start(startDeps);
|
||||
|
||||
await navigateToApp('app1', { deepLinkId: 'dl-unknown' });
|
||||
expect(MockHistory.push).toHaveBeenLastCalledWith('/app/app1/default/path', undefined);
|
||||
|
||||
await navigateToApp('app1', { deepLinkId: 'dl-unknown', path: 'some-other-path' });
|
||||
expect(MockHistory.push).toHaveBeenLastCalledWith('/app/app1/some-other-path', undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('navigateToUrl', () => {
|
||||
|
|
|
@ -64,6 +64,10 @@ const getAppUrl = (mounters: Map<string, Mounter>, appId: string, path: string =
|
|||
return appendAppPath(appBasePath, path);
|
||||
};
|
||||
|
||||
const getAppDeepLinkPath = (mounters: Map<string, Mounter>, appId: string, deepLinkId: string) => {
|
||||
return mounters.get(appId)?.deepLinkPaths[deepLinkId];
|
||||
};
|
||||
|
||||
const allApplicationsFilter = '__ALL__';
|
||||
|
||||
interface AppUpdaterWrapper {
|
||||
|
@ -175,6 +179,7 @@ export class ApplicationService {
|
|||
this.mounters.set(app.id, {
|
||||
appRoute: app.appRoute!,
|
||||
appBasePath: basePath.prepend(app.appRoute!),
|
||||
deepLinkPaths: toDeepLinkPaths(app.deepLinks),
|
||||
exactRoute: app.exactRoute ?? false,
|
||||
mount: wrapMount(plugin, app),
|
||||
unmountBeforeMounting: false,
|
||||
|
@ -226,7 +231,7 @@ export class ApplicationService {
|
|||
|
||||
const navigateToApp: InternalApplicationStart['navigateToApp'] = async (
|
||||
appId,
|
||||
{ path, state, replace = false, openInNewTab = false }: NavigateToAppOptions = {}
|
||||
{ deepLinkId, path, state, replace = false, openInNewTab = false }: NavigateToAppOptions = {}
|
||||
) => {
|
||||
const currentAppId = this.currentAppId$.value;
|
||||
const navigatingToSameApp = currentAppId === appId;
|
||||
|
@ -235,6 +240,12 @@ export class ApplicationService {
|
|||
: await this.shouldNavigate(overlays, appId);
|
||||
|
||||
if (shouldNavigate) {
|
||||
if (deepLinkId) {
|
||||
const deepLinkPath = getAppDeepLinkPath(availableMounters, appId, deepLinkId);
|
||||
if (deepLinkPath) {
|
||||
path = appendAppPath(deepLinkPath, path);
|
||||
}
|
||||
}
|
||||
if (path === undefined) {
|
||||
path = applications$.value.get(appId)?.defaultPath;
|
||||
}
|
||||
|
@ -384,8 +395,18 @@ const updateStatus = (app: App, statusUpdaters: AppUpdaterWrapper[]): App => {
|
|||
...fields,
|
||||
// status and navLinkStatus enums are ordered by reversed priority
|
||||
// if multiple updaters wants to change these fields, we will always follow the priority order.
|
||||
status: Math.max(changes.status ?? 0, fields.status ?? 0),
|
||||
navLinkStatus: Math.max(changes.navLinkStatus ?? 0, fields.navLinkStatus ?? 0),
|
||||
status: Math.max(
|
||||
changes.status ?? AppStatus.accessible,
|
||||
fields.status ?? AppStatus.accessible
|
||||
),
|
||||
navLinkStatus: Math.max(
|
||||
changes.navLinkStatus ?? AppNavLinkStatus.default,
|
||||
fields.navLinkStatus ?? AppNavLinkStatus.default
|
||||
),
|
||||
// deepLinks take the last defined update
|
||||
deepLinks: fields.deepLinks
|
||||
? populateDeepLinkDefaults(fields.deepLinks)
|
||||
: changes.deepLinks,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -396,10 +417,22 @@ const updateStatus = (app: App, statusUpdaters: AppUpdaterWrapper[]): App => {
|
|||
};
|
||||
|
||||
const populateDeepLinkDefaults = (deepLinks?: AppDeepLink[]): AppDeepLink[] => {
|
||||
if (!deepLinks) return [];
|
||||
if (!deepLinks) {
|
||||
return [];
|
||||
}
|
||||
return deepLinks.map((deepLink) => ({
|
||||
...deepLink,
|
||||
navLinkStatus: deepLink.navLinkStatus ?? AppNavLinkStatus.default,
|
||||
deepLinks: populateDeepLinkDefaults(deepLink.deepLinks),
|
||||
}));
|
||||
};
|
||||
|
||||
const toDeepLinkPaths = (deepLinks?: AppDeepLink[]): Mounter['deepLinkPaths'] => {
|
||||
if (!deepLinks) {
|
||||
return {};
|
||||
}
|
||||
return deepLinks.reduce((deepLinkPaths: Mounter['deepLinkPaths'], deepLink) => {
|
||||
if (deepLink.path) deepLinkPaths[deepLink.id] = deepLink.path;
|
||||
return { ...deepLinkPaths, ...toDeepLinkPaths(deepLink.deepLinks) };
|
||||
}, {});
|
||||
};
|
||||
|
|
|
@ -17,6 +17,7 @@ export type {
|
|||
AppUnmount,
|
||||
AppMountParameters,
|
||||
AppUpdatableFields,
|
||||
AppNavOptions,
|
||||
AppUpdater,
|
||||
AppDeepLink,
|
||||
ApplicationSetup,
|
||||
|
|
|
@ -35,12 +35,14 @@ export const createAppMounter = ({
|
|||
appId,
|
||||
html = `<div>App ${appId}</div>`,
|
||||
appRoute = `/app/${appId}`,
|
||||
deepLinkPaths = {},
|
||||
exactRoute = false,
|
||||
extraMountHook,
|
||||
}: {
|
||||
appId: string;
|
||||
html?: string;
|
||||
appRoute?: string;
|
||||
deepLinkPaths?: Record<string, string>;
|
||||
exactRoute?: boolean;
|
||||
extraMountHook?: (params: AppMountParameters) => void;
|
||||
}): MockedMounterTuple => {
|
||||
|
@ -51,6 +53,7 @@ export const createAppMounter = ({
|
|||
mounter: {
|
||||
appRoute,
|
||||
appBasePath: appRoute,
|
||||
deepLinkPaths,
|
||||
exactRoute,
|
||||
mount: jest.fn(async (params: AppMountParameters) => {
|
||||
const { appBasePath: basename, element } = params;
|
||||
|
|
|
@ -63,9 +63,37 @@ export enum AppNavLinkStatus {
|
|||
*/
|
||||
export type AppUpdatableFields = Pick<
|
||||
App,
|
||||
'status' | 'navLinkStatus' | 'tooltip' | 'defaultPath' | 'deepLinks'
|
||||
'status' | 'navLinkStatus' | 'searchable' | 'tooltip' | 'defaultPath' | 'deepLinks'
|
||||
>;
|
||||
|
||||
/**
|
||||
* App navigation menu options
|
||||
* @public
|
||||
*/
|
||||
export interface AppNavOptions {
|
||||
/**
|
||||
* An ordinal used to sort nav links relative to one another for display.
|
||||
*/
|
||||
order?: number;
|
||||
|
||||
/**
|
||||
* A tooltip shown when hovering over app link.
|
||||
*/
|
||||
tooltip?: string;
|
||||
|
||||
/**
|
||||
* A EUI iconType that will be used for the app's icon. This icon
|
||||
* takes precendence over the `icon` property.
|
||||
*/
|
||||
euiIconType?: string;
|
||||
|
||||
/**
|
||||
* A URL to an image file used as an icon. Used as a fallback
|
||||
* if `euiIconType` is not provided.
|
||||
*/
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updater for applications.
|
||||
* see {@link ApplicationSetup}
|
||||
|
@ -76,7 +104,7 @@ export type AppUpdater = (app: App) => Partial<AppUpdatableFields> | undefined;
|
|||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface App<HistoryLocationState = unknown> {
|
||||
export interface App<HistoryLocationState = unknown> extends AppNavOptions {
|
||||
/**
|
||||
* The unique identifier of the application
|
||||
*/
|
||||
|
@ -107,6 +135,12 @@ export interface App<HistoryLocationState = unknown> {
|
|||
*/
|
||||
navLinkStatus?: AppNavLinkStatus;
|
||||
|
||||
/**
|
||||
* The initial flag to determine if the application is searchable in the global search.
|
||||
* Defaulting to `true` if `navLinkStatus` is `visible` or omitted.
|
||||
*/
|
||||
searchable?: boolean;
|
||||
|
||||
/**
|
||||
* 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 {@link ApplicationStart.navigateToApp | navigateToApp}`,
|
||||
|
@ -148,28 +182,6 @@ export interface App<HistoryLocationState = unknown> {
|
|||
*/
|
||||
updater$?: Observable<AppUpdater>;
|
||||
|
||||
/**
|
||||
* An ordinal used to sort nav links relative to one another for display.
|
||||
*/
|
||||
order?: number;
|
||||
|
||||
/**
|
||||
* A tooltip shown when hovering over app link.
|
||||
*/
|
||||
tooltip?: string;
|
||||
|
||||
/**
|
||||
* A EUI iconType that will be used for the app's icon. This icon
|
||||
* takes precendence over the `icon` property.
|
||||
*/
|
||||
euiIconType?: string;
|
||||
|
||||
/**
|
||||
* A URL to an image file used as an icon. Used as a fallback
|
||||
* if `euiIconType` is not provided.
|
||||
*/
|
||||
icon?: string;
|
||||
|
||||
/**
|
||||
* Custom capabilities defined by the app.
|
||||
*/
|
||||
|
@ -261,11 +273,12 @@ export interface App<HistoryLocationState = unknown> {
|
|||
*/
|
||||
export type PublicAppDeepLinkInfo = Omit<
|
||||
AppDeepLink,
|
||||
'deepLinks' | 'keywords' | 'navLinkStatus'
|
||||
'deepLinks' | 'keywords' | 'navLinkStatus' | 'searchable'
|
||||
> & {
|
||||
deepLinks: PublicAppDeepLinkInfo[];
|
||||
keywords: string[];
|
||||
navLinkStatus: AppNavLinkStatus;
|
||||
searchable: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -285,33 +298,40 @@ export type AppDeepLink = {
|
|||
keywords?: string[];
|
||||
/** Optional status of the chrome navigation, defaults to `hidden` */
|
||||
navLinkStatus?: AppNavLinkStatus;
|
||||
} & (
|
||||
| {
|
||||
/** URL path to access this link, relative to the application's appRoute. */
|
||||
path: string;
|
||||
/** Optional array of links that are 'underneath' this section in the hierarchy */
|
||||
deepLinks?: AppDeepLink[];
|
||||
}
|
||||
| {
|
||||
/** Optional path to access this section. Omit if this part of the hierarchy does not have a page URL. */
|
||||
path?: string;
|
||||
/** Array links that are 'underneath' this section in this hierarchy. */
|
||||
deepLinks: AppDeepLink[];
|
||||
}
|
||||
);
|
||||
/** Optional flag to determine if the link is searchable in the global search. Defaulting to `true` if `navLinkStatus` is `visible` or omitted */
|
||||
searchable?: boolean;
|
||||
} & AppNavOptions &
|
||||
(
|
||||
| {
|
||||
/** URL path to access this link, relative to the application's appRoute. */
|
||||
path: string;
|
||||
/** Optional array of links that are 'underneath' this section in the hierarchy */
|
||||
deepLinks?: AppDeepLink[];
|
||||
}
|
||||
| {
|
||||
/** Optional path to access this section. Omit if this part of the hierarchy does not have a page URL. */
|
||||
path?: string;
|
||||
/** Array links that are 'underneath' this section in this hierarchy. */
|
||||
deepLinks: AppDeepLink[];
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Public information about a registered {@link App | application}
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type PublicAppInfo = Omit<App, 'mount' | 'updater$' | 'keywords' | 'deepLinks'> & {
|
||||
export type PublicAppInfo = Omit<
|
||||
App,
|
||||
'mount' | 'updater$' | 'keywords' | 'deepLinks' | 'searchable'
|
||||
> & {
|
||||
// remove optional on fields populated with default values
|
||||
status: AppStatus;
|
||||
navLinkStatus: AppNavLinkStatus;
|
||||
appRoute: string;
|
||||
keywords: string[];
|
||||
deepLinks: PublicAppDeepLinkInfo[];
|
||||
searchable: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -592,6 +612,7 @@ export interface AppLeaveActionFactory {
|
|||
export interface Mounter {
|
||||
appRoute: string;
|
||||
appBasePath: string;
|
||||
deepLinkPaths: Record<string, string>;
|
||||
mount: AppMount;
|
||||
exactRoute: boolean;
|
||||
unmountBeforeMounting?: boolean;
|
||||
|
@ -657,11 +678,17 @@ export interface InternalApplicationSetup extends Pick<ApplicationSetup, 'regist
|
|||
|
||||
/**
|
||||
* Options for the {@link ApplicationStart.navigateToApp | navigateToApp API}
|
||||
* @public
|
||||
*/
|
||||
export interface NavigateToAppOptions {
|
||||
/**
|
||||
* optional {@link App.deepLinks | deep link} id inside the application to navigate to.
|
||||
* If an additional {@link NavigateToAppOptions.path | path} is defined it will be appended to the deep link path.
|
||||
*/
|
||||
deepLinkId?: string;
|
||||
/**
|
||||
* optional path inside application to deep link to.
|
||||
* If undefined, will use {@link App.defaultPath | the app's default path}` as default.
|
||||
* If undefined, will use {@link App.defaultPath | the app's default path} as default.
|
||||
*/
|
||||
path?: string;
|
||||
/**
|
||||
|
|
|
@ -45,6 +45,7 @@ describe('AppContainer', () => {
|
|||
appRoute: '/some-route',
|
||||
unmountBeforeMounting: false,
|
||||
exactRoute: false,
|
||||
deepLinkPaths: {},
|
||||
mount: async ({ element }: AppMountParameters) => {
|
||||
await promise;
|
||||
const container = document.createElement('div');
|
||||
|
@ -133,6 +134,7 @@ describe('AppContainer', () => {
|
|||
const mounter = {
|
||||
appBasePath: '/base-path/some-route',
|
||||
appRoute: '/some-route',
|
||||
deepLinkPaths: {},
|
||||
unmountBeforeMounting: false,
|
||||
exactRoute: false,
|
||||
mount: async ({ element }: AppMountParameters) => {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import { of } from 'rxjs';
|
||||
import { App, AppNavLinkStatus, AppStatus } from '../types';
|
||||
import { App, AppDeepLink, AppNavLinkStatus, AppStatus } from '../types';
|
||||
import { getAppInfo } from './get_app_info';
|
||||
|
||||
describe('getAppInfo', () => {
|
||||
|
@ -18,10 +18,22 @@ describe('getAppInfo', () => {
|
|||
title: 'some-title',
|
||||
status: AppStatus.accessible,
|
||||
navLinkStatus: AppNavLinkStatus.default,
|
||||
searchable: true,
|
||||
appRoute: `/app/some-id`,
|
||||
...props,
|
||||
});
|
||||
|
||||
const createDeepLink = (props: Partial<AppDeepLink> = {}): AppDeepLink => ({
|
||||
id: 'some-deep-link-id',
|
||||
title: 'my deep link',
|
||||
path: '/my-deep-link',
|
||||
navLinkStatus: AppNavLinkStatus.default,
|
||||
searchable: true,
|
||||
deepLinks: [],
|
||||
keywords: [],
|
||||
...props,
|
||||
});
|
||||
|
||||
it('converts an application and remove sensitive properties', () => {
|
||||
const app = createApp();
|
||||
const info = getAppInfo(app);
|
||||
|
@ -31,6 +43,7 @@ describe('getAppInfo', () => {
|
|||
title: 'some-title',
|
||||
status: AppStatus.accessible,
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
searchable: true,
|
||||
appRoute: `/app/some-id`,
|
||||
keywords: [],
|
||||
deepLinks: [],
|
||||
|
@ -54,12 +67,15 @@ describe('getAppInfo', () => {
|
|||
title: 'some-title',
|
||||
status: AppStatus.accessible,
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
searchable: true,
|
||||
appRoute: `/app/some-id`,
|
||||
keywords: [],
|
||||
deepLinks: [
|
||||
{
|
||||
id: 'sub-id',
|
||||
title: 'sub-title',
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
searchable: true,
|
||||
keywords: [],
|
||||
deepLinks: [
|
||||
{
|
||||
|
@ -67,6 +83,8 @@ describe('getAppInfo', () => {
|
|||
title: 'sub-sub-title',
|
||||
path: '/sub-sub',
|
||||
keywords: [],
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
searchable: true,
|
||||
deepLinks: [], // default empty array added
|
||||
},
|
||||
],
|
||||
|
@ -102,7 +120,70 @@ describe('getAppInfo', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('adds default meta fields to sublinks when needed', () => {
|
||||
it('computes the searchable flag depending on the navLinkStatus when needed', () => {
|
||||
expect(
|
||||
getAppInfo(
|
||||
createApp({
|
||||
navLinkStatus: AppNavLinkStatus.default,
|
||||
searchable: undefined,
|
||||
})
|
||||
)
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
searchable: true,
|
||||
})
|
||||
);
|
||||
expect(
|
||||
getAppInfo(
|
||||
createApp({
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
searchable: undefined,
|
||||
})
|
||||
)
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
searchable: true,
|
||||
})
|
||||
);
|
||||
expect(
|
||||
getAppInfo(
|
||||
createApp({
|
||||
navLinkStatus: AppNavLinkStatus.disabled,
|
||||
searchable: undefined,
|
||||
})
|
||||
)
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
searchable: false,
|
||||
})
|
||||
);
|
||||
expect(
|
||||
getAppInfo(
|
||||
createApp({
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
searchable: undefined,
|
||||
})
|
||||
)
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
searchable: false,
|
||||
})
|
||||
);
|
||||
expect(
|
||||
getAppInfo(
|
||||
createApp({
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
searchable: true,
|
||||
})
|
||||
)
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
searchable: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('adds default deepLinks when needed', () => {
|
||||
const app = createApp({
|
||||
deepLinks: [
|
||||
{
|
||||
|
@ -126,17 +207,22 @@ describe('getAppInfo', () => {
|
|||
title: 'some-title',
|
||||
status: AppStatus.accessible,
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
searchable: true,
|
||||
appRoute: `/app/some-id`,
|
||||
keywords: [],
|
||||
deepLinks: [
|
||||
{
|
||||
id: 'sub-id',
|
||||
title: 'sub-title',
|
||||
keywords: [], // default empty array
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
searchable: true,
|
||||
keywords: [],
|
||||
deepLinks: [
|
||||
{
|
||||
id: 'sub-sub-id',
|
||||
title: 'sub-sub-title',
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
searchable: true,
|
||||
path: '/sub-sub',
|
||||
keywords: ['sub sub'],
|
||||
deepLinks: [],
|
||||
|
@ -146,4 +232,127 @@ describe('getAppInfo', () => {
|
|||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('computes the deepLinks navLinkStatus when needed', () => {
|
||||
expect(
|
||||
getAppInfo(
|
||||
createApp({
|
||||
deepLinks: [
|
||||
createDeepLink({
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
deepLinks: [
|
||||
expect.objectContaining({
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
expect(
|
||||
getAppInfo(
|
||||
createApp({
|
||||
deepLinks: [
|
||||
createDeepLink({
|
||||
navLinkStatus: AppNavLinkStatus.default,
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
deepLinks: [
|
||||
expect.objectContaining({
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
expect(
|
||||
getAppInfo(
|
||||
createApp({
|
||||
deepLinks: [
|
||||
createDeepLink({
|
||||
navLinkStatus: undefined,
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
deepLinks: [
|
||||
expect.objectContaining({
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('computes the deepLinks searchable depending on the navLinkStatus when needed', () => {
|
||||
expect(
|
||||
getAppInfo(
|
||||
createApp({
|
||||
deepLinks: [
|
||||
createDeepLink({
|
||||
navLinkStatus: AppNavLinkStatus.default,
|
||||
searchable: undefined,
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
deepLinks: [
|
||||
expect.objectContaining({
|
||||
searchable: true,
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
expect(
|
||||
getAppInfo(
|
||||
createApp({
|
||||
deepLinks: [
|
||||
createDeepLink({
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
searchable: undefined,
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
deepLinks: [
|
||||
expect.objectContaining({
|
||||
searchable: false,
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
expect(
|
||||
getAppInfo(
|
||||
createApp({
|
||||
deepLinks: [
|
||||
createDeepLink({
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
searchable: true,
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
deepLinks: [
|
||||
expect.objectContaining({
|
||||
searchable: true,
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,17 +16,19 @@ import {
|
|||
} 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;
|
||||
const { updater$, mount, navLinkStatus = AppNavLinkStatus.default, ...infos } = app;
|
||||
return {
|
||||
...infos,
|
||||
status: app.status!,
|
||||
navLinkStatus,
|
||||
navLinkStatus:
|
||||
navLinkStatus === AppNavLinkStatus.default
|
||||
? app.status === AppStatus.inaccessible
|
||||
? AppNavLinkStatus.hidden
|
||||
: AppNavLinkStatus.visible
|
||||
: navLinkStatus,
|
||||
searchable:
|
||||
app.searchable ??
|
||||
(navLinkStatus === AppNavLinkStatus.default || navLinkStatus === AppNavLinkStatus.visible),
|
||||
appRoute: app.appRoute!,
|
||||
keywords: app.keywords ?? [],
|
||||
deepLinks: getDeepLinkInfos(app.deepLinks),
|
||||
|
@ -37,17 +39,18 @@ function getDeepLinkInfos(deepLinks?: AppDeepLink[]): PublicAppDeepLinkInfo[] {
|
|||
if (!deepLinks) return [];
|
||||
|
||||
return deepLinks.map(
|
||||
(rawDeepLink): PublicAppDeepLinkInfo => {
|
||||
const navLinkStatus =
|
||||
rawDeepLink.navLinkStatus === AppNavLinkStatus.default
|
||||
? AppNavLinkStatus.hidden
|
||||
: rawDeepLink.navLinkStatus!;
|
||||
({ navLinkStatus = AppNavLinkStatus.default, ...rawDeepLink }): PublicAppDeepLinkInfo => {
|
||||
return {
|
||||
id: rawDeepLink.id,
|
||||
title: rawDeepLink.title,
|
||||
path: rawDeepLink.path,
|
||||
keywords: rawDeepLink.keywords ?? [],
|
||||
navLinkStatus,
|
||||
navLinkStatus:
|
||||
navLinkStatus === AppNavLinkStatus.default ? AppNavLinkStatus.hidden : navLinkStatus,
|
||||
searchable:
|
||||
rawDeepLink.searchable ??
|
||||
(navLinkStatus === AppNavLinkStatus.default ||
|
||||
navLinkStatus === AppNavLinkStatus.visible),
|
||||
deepLinks: getDeepLinkInfos(rawDeepLink.deepLinks),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -33,10 +33,9 @@ export interface ChromeNavLink {
|
|||
readonly baseUrl: string;
|
||||
|
||||
/**
|
||||
* The route used to open the {@link AppBase.defaultPath | default path } of an application.
|
||||
* If unset, `baseUrl` will be used instead.
|
||||
* The route used to open the default path and the deep links of an application.
|
||||
*/
|
||||
readonly url?: string;
|
||||
readonly url: string;
|
||||
|
||||
/**
|
||||
* An ordinal used to sort nav links relative to one another for display.
|
||||
|
|
|
@ -20,6 +20,22 @@ const availableApps = new Map([
|
|||
order: -10,
|
||||
title: 'App 2',
|
||||
euiIconType: 'canvasApp',
|
||||
deepLinks: [
|
||||
{
|
||||
id: 'deepApp1',
|
||||
order: 50,
|
||||
title: 'Deep App 1',
|
||||
path: '/deepapp1',
|
||||
deepLinks: [
|
||||
{
|
||||
id: 'deepApp2',
|
||||
order: 40,
|
||||
title: 'Deep App 2',
|
||||
path: '/deepapp2',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
['chromelessApp', { id: 'chromelessApp', order: 20, title: 'Chromless App', chromeless: true }],
|
||||
|
@ -66,7 +82,7 @@ describe('NavLinksService', () => {
|
|||
map((links) => links.map((l) => l.id))
|
||||
)
|
||||
.toPromise()
|
||||
).toEqual(['app2', 'app1']);
|
||||
).toEqual(['app2', 'app1', 'app2:deepApp2', 'app2:deepApp1']);
|
||||
});
|
||||
|
||||
it('emits multiple values', async () => {
|
||||
|
@ -76,7 +92,7 @@ describe('NavLinksService', () => {
|
|||
start.showOnly('app1');
|
||||
|
||||
service.stop();
|
||||
expect(emittedLinks).toEqual([['app2', 'app1'], ['app1']]);
|
||||
expect(emittedLinks).toEqual([['app2', 'app1', 'app2:deepApp2', 'app2:deepApp1'], ['app1']]);
|
||||
});
|
||||
|
||||
it('completes when service is stopped', async () => {
|
||||
|
@ -98,7 +114,12 @@ describe('NavLinksService', () => {
|
|||
|
||||
describe('#getAll()', () => {
|
||||
it('returns a sorted array of navlinks', () => {
|
||||
expect(start.getAll().map((l) => l.id)).toEqual(['app2', 'app1']);
|
||||
expect(start.getAll().map((l) => l.id)).toEqual([
|
||||
'app2',
|
||||
'app1',
|
||||
'app2:deepApp2',
|
||||
'app2:deepApp1',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -123,7 +144,7 @@ describe('NavLinksService', () => {
|
|||
map((links) => links.map((l) => l.id))
|
||||
)
|
||||
.toPromise()
|
||||
).toEqual(['app2', 'app1']);
|
||||
).toEqual(['app2', 'app1', 'app2:deepApp2', 'app2:deepApp1']);
|
||||
});
|
||||
|
||||
it('does nothing on chromeless applications', async () => {
|
||||
|
@ -136,7 +157,7 @@ describe('NavLinksService', () => {
|
|||
map((links) => links.map((l) => l.id))
|
||||
)
|
||||
.toPromise()
|
||||
).toEqual(['app2', 'app1']);
|
||||
).toEqual(['app2', 'app1', 'app2:deepApp2', 'app2:deepApp1']);
|
||||
});
|
||||
|
||||
it('removes all other links', async () => {
|
||||
|
@ -152,6 +173,19 @@ describe('NavLinksService', () => {
|
|||
).toEqual(['app2']);
|
||||
});
|
||||
|
||||
it('show only deep link', async () => {
|
||||
start.showOnly('app2:deepApp1');
|
||||
expect(
|
||||
await start
|
||||
.getNavLinks$()
|
||||
.pipe(
|
||||
take(1),
|
||||
map((links) => links.map((l) => l.id))
|
||||
)
|
||||
.toPromise()
|
||||
).toEqual(['app2:deepApp1']);
|
||||
});
|
||||
|
||||
it('still removes all other links when availableApps are re-emitted', async () => {
|
||||
start.showOnly('app2');
|
||||
mockAppService.applications$.next(mockAppService.applications$.value);
|
||||
|
|
|
@ -10,8 +10,8 @@ import { sortBy } from 'lodash';
|
|||
import { BehaviorSubject, combineLatest, Observable, ReplaySubject } from 'rxjs';
|
||||
import { map, takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { InternalApplicationStart } from '../../application';
|
||||
import { HttpStart } from '../../http';
|
||||
import { InternalApplicationStart, PublicAppDeepLinkInfo, PublicAppInfo } from '../../application';
|
||||
import { HttpStart, IBasePath } from '../../http';
|
||||
import { ChromeNavLink, NavLinkWrapper } from './nav_link';
|
||||
import { toNavLink } from './to_nav_link';
|
||||
|
||||
|
@ -89,7 +89,13 @@ export class NavLinksService {
|
|||
return new Map(
|
||||
[...apps]
|
||||
.filter(([, app]) => !app.chromeless)
|
||||
.map(([appId, app]) => [appId, toNavLink(app, http.basePath)])
|
||||
.reduce((navLinks: Array<[string, NavLinkWrapper]>, [appId, app]) => {
|
||||
navLinks.push(
|
||||
[appId, toNavLink(app, http.basePath)],
|
||||
...toNavDeepLinks(app, app.deepLinks, http.basePath)
|
||||
);
|
||||
return navLinks;
|
||||
}, [])
|
||||
);
|
||||
})
|
||||
);
|
||||
|
@ -163,3 +169,21 @@ function sortNavLinks(navLinks: ReadonlyMap<string, NavLinkWrapper>) {
|
|||
'order'
|
||||
);
|
||||
}
|
||||
|
||||
function toNavDeepLinks(
|
||||
app: PublicAppInfo,
|
||||
deepLinks: PublicAppDeepLinkInfo[],
|
||||
basePath: IBasePath
|
||||
): Array<[string, NavLinkWrapper]> {
|
||||
if (!deepLinks) {
|
||||
return [];
|
||||
}
|
||||
return deepLinks.reduce((navDeepLinks: Array<[string, NavLinkWrapper]>, deepLink) => {
|
||||
const id = `${app.id}:${deepLink.id}`;
|
||||
if (deepLink.path) {
|
||||
navDeepLinks.push([id, toNavLink(app, basePath, { ...deepLink, id })]);
|
||||
}
|
||||
navDeepLinks.push(...toNavDeepLinks(app, deepLink.deepLinks, basePath));
|
||||
return navDeepLinks;
|
||||
}, []);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,12 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { PublicAppInfo, AppNavLinkStatus, AppStatus } from '../../application';
|
||||
import {
|
||||
PublicAppInfo,
|
||||
AppNavLinkStatus,
|
||||
AppStatus,
|
||||
PublicAppDeepLinkInfo,
|
||||
} from '../../application';
|
||||
import { toNavLink } from './to_nav_link';
|
||||
|
||||
import { httpServiceMock } from '../../mocks';
|
||||
|
@ -16,12 +21,24 @@ const app = (props: Partial<PublicAppInfo> = {}): PublicAppInfo => ({
|
|||
title: 'some-title',
|
||||
status: AppStatus.accessible,
|
||||
navLinkStatus: AppNavLinkStatus.default,
|
||||
searchable: true,
|
||||
appRoute: `/app/some-id`,
|
||||
keywords: [],
|
||||
deepLinks: [],
|
||||
...props,
|
||||
});
|
||||
|
||||
const deepLink = (props: Partial<PublicAppDeepLinkInfo> = {}): PublicAppDeepLinkInfo => ({
|
||||
id: 'some-deep-link-id',
|
||||
title: 'my deep link',
|
||||
path: '/my-deep-link',
|
||||
navLinkStatus: AppNavLinkStatus.default,
|
||||
searchable: true,
|
||||
deepLinks: [],
|
||||
keywords: [],
|
||||
...props,
|
||||
});
|
||||
|
||||
describe('toNavLink', () => {
|
||||
const basePath = httpServiceMock.createSetupContract({ basePath: '/base-path' }).basePath;
|
||||
|
||||
|
@ -64,7 +81,7 @@ describe('toNavLink', () => {
|
|||
}),
|
||||
basePath
|
||||
);
|
||||
expect(link.properties.url).toEqual('http://localhost/base-path/my-route/my-path');
|
||||
expect(link.properties.url).toEqual('/base-path/my-route/my-path');
|
||||
|
||||
link = toNavLink(
|
||||
app({
|
||||
|
@ -73,9 +90,7 @@ describe('toNavLink', () => {
|
|||
}),
|
||||
basePath
|
||||
);
|
||||
expect(link.properties.url).toEqual(
|
||||
'http://localhost/base-path/my-route/my-path/some/default/path'
|
||||
);
|
||||
expect(link.properties.url).toEqual('/base-path/my-route/my-path/some/default/path');
|
||||
});
|
||||
|
||||
it('uses the application status when the navLinkStatus is set to default', () => {
|
||||
|
@ -153,4 +168,90 @@ describe('toNavLink', () => {
|
|||
})
|
||||
);
|
||||
});
|
||||
|
||||
describe('deepLink parameter', () => {
|
||||
it('should be hidden and not disabled by default', () => {
|
||||
expect(toNavLink(app(), basePath, deepLink()).properties).toEqual(
|
||||
expect.objectContaining({
|
||||
disabled: false,
|
||||
hidden: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should not be hidden when navLinkStatus is visible', () => {
|
||||
expect(
|
||||
toNavLink(
|
||||
app(),
|
||||
basePath,
|
||||
deepLink({
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
})
|
||||
).properties
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
disabled: false,
|
||||
hidden: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should be disabled when navLinkStatus is disabled', () => {
|
||||
expect(
|
||||
toNavLink(
|
||||
app(),
|
||||
basePath,
|
||||
deepLink({
|
||||
navLinkStatus: AppNavLinkStatus.disabled,
|
||||
})
|
||||
).properties
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
disabled: true,
|
||||
hidden: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should have href, baseUrl and url containing the path', () => {
|
||||
const testApp = app({
|
||||
appRoute: '/app/app-id',
|
||||
defaultPath: '/default-path',
|
||||
});
|
||||
|
||||
expect(toNavLink(testApp, basePath).properties).toEqual(
|
||||
expect.objectContaining({
|
||||
baseUrl: 'http://localhost/base-path/app/app-id',
|
||||
url: '/base-path/app/app-id/default-path',
|
||||
href: 'http://localhost/base-path/app/app-id/default-path',
|
||||
})
|
||||
);
|
||||
|
||||
expect(
|
||||
toNavLink(
|
||||
testApp,
|
||||
basePath,
|
||||
deepLink({
|
||||
id: 'deep-link-id',
|
||||
path: '/my-deep-link',
|
||||
})
|
||||
).properties
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
baseUrl: 'http://localhost/base-path/app/app-id',
|
||||
url: '/base-path/app/app-id/my-deep-link',
|
||||
href: 'http://localhost/base-path/app/app-id/my-deep-link',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should use the main app category', () => {
|
||||
expect(toNavLink(app(), basePath, deepLink()).properties.category).toBeUndefined();
|
||||
|
||||
const category = { id: 'some-category', label: 'some category' };
|
||||
expect(toNavLink(app({ category }), basePath, deepLink()).properties.category).toEqual(
|
||||
category
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,29 +6,50 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { PublicAppInfo, AppNavLinkStatus, AppStatus } from '../../application';
|
||||
import {
|
||||
PublicAppInfo,
|
||||
AppNavLinkStatus,
|
||||
AppStatus,
|
||||
PublicAppDeepLinkInfo,
|
||||
} from '../../application';
|
||||
import { IBasePath } from '../../http';
|
||||
import { NavLinkWrapper } from './nav_link';
|
||||
import { appendAppPath } from '../../application/utils';
|
||||
|
||||
export function toNavLink(app: PublicAppInfo, basePath: IBasePath): NavLinkWrapper {
|
||||
const useAppStatus = app.navLinkStatus === AppNavLinkStatus.default;
|
||||
export function toNavLink(
|
||||
app: PublicAppInfo,
|
||||
basePath: IBasePath,
|
||||
deepLink?: PublicAppDeepLinkInfo
|
||||
): NavLinkWrapper {
|
||||
const relativeBaseUrl = basePath.prepend(app.appRoute!);
|
||||
const url = relativeToAbsolute(appendAppPath(relativeBaseUrl, app.defaultPath));
|
||||
const url = appendAppPath(relativeBaseUrl, deepLink?.path || app.defaultPath);
|
||||
const href = relativeToAbsolute(url);
|
||||
const baseUrl = relativeToAbsolute(relativeBaseUrl);
|
||||
|
||||
return new NavLinkWrapper({
|
||||
...app,
|
||||
hidden: useAppStatus
|
||||
? app.status === AppStatus.inaccessible
|
||||
: app.navLinkStatus === AppNavLinkStatus.hidden,
|
||||
disabled: useAppStatus ? false : app.navLinkStatus === AppNavLinkStatus.disabled,
|
||||
...(deepLink || app),
|
||||
...(app.category ? { category: app.category } : {}), // deepLinks use the main app category
|
||||
hidden: deepLink ? isDeepNavLinkHidden(deepLink) : isAppNavLinkHidden(app),
|
||||
disabled: (deepLink?.navLinkStatus ?? app.navLinkStatus) === AppNavLinkStatus.disabled,
|
||||
baseUrl,
|
||||
href: url,
|
||||
href,
|
||||
url,
|
||||
});
|
||||
}
|
||||
|
||||
function isAppNavLinkHidden(app: PublicAppInfo) {
|
||||
return app.navLinkStatus === AppNavLinkStatus.default
|
||||
? app.status === AppStatus.inaccessible
|
||||
: app.navLinkStatus === AppNavLinkStatus.hidden;
|
||||
}
|
||||
|
||||
function isDeepNavLinkHidden(deepLink: PublicAppDeepLinkInfo) {
|
||||
return (
|
||||
deepLink.navLinkStatus === AppNavLinkStatus.default ||
|
||||
deepLink.navLinkStatus === AppNavLinkStatus.hidden
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} url - a relative or root relative url. If a relative path is given then the
|
||||
* absolute url returned will depend on the current page where this function is called from. For example
|
||||
|
|
|
@ -73,6 +73,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
|
|||
"id": "Custom link",
|
||||
"isActive": true,
|
||||
"title": "Custom link",
|
||||
"url": "/",
|
||||
},
|
||||
"closed": false,
|
||||
"hasError": false,
|
||||
|
@ -140,6 +141,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
|
|||
"id": "discover",
|
||||
"isActive": true,
|
||||
"title": "discover",
|
||||
"url": "/",
|
||||
},
|
||||
Object {
|
||||
"baseUrl": "/",
|
||||
|
@ -154,6 +156,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
|
|||
"id": "siem",
|
||||
"isActive": true,
|
||||
"title": "siem",
|
||||
"url": "/",
|
||||
},
|
||||
Object {
|
||||
"baseUrl": "/",
|
||||
|
@ -168,6 +171,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
|
|||
"id": "metrics",
|
||||
"isActive": true,
|
||||
"title": "metrics",
|
||||
"url": "/",
|
||||
},
|
||||
Object {
|
||||
"baseUrl": "/",
|
||||
|
@ -182,6 +186,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
|
|||
"id": "monitoring",
|
||||
"isActive": true,
|
||||
"title": "monitoring",
|
||||
"url": "/",
|
||||
},
|
||||
Object {
|
||||
"baseUrl": "/",
|
||||
|
@ -196,6 +201,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
|
|||
"id": "visualize",
|
||||
"isActive": true,
|
||||
"title": "visualize",
|
||||
"url": "/",
|
||||
},
|
||||
Object {
|
||||
"baseUrl": "/",
|
||||
|
@ -210,6 +216,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
|
|||
"id": "dashboard",
|
||||
"isActive": true,
|
||||
"title": "dashboard",
|
||||
"url": "/",
|
||||
},
|
||||
Object {
|
||||
"baseUrl": "/",
|
||||
|
@ -219,6 +226,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
|
|||
"id": "canvas",
|
||||
"isActive": true,
|
||||
"title": "canvas",
|
||||
"url": "/",
|
||||
},
|
||||
Object {
|
||||
"baseUrl": "/",
|
||||
|
@ -233,6 +241,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
|
|||
"id": "logs",
|
||||
"isActive": true,
|
||||
"title": "logs",
|
||||
"url": "/",
|
||||
},
|
||||
],
|
||||
"closed": false,
|
||||
|
@ -351,6 +360,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
|
|||
"values": Array [],
|
||||
}
|
||||
}
|
||||
url="/"
|
||||
>
|
||||
<EuiCollapsibleNav
|
||||
aria-label="Primary"
|
||||
|
@ -2274,6 +2284,7 @@ exports[`CollapsibleNav renders the default nav 1`] = `
|
|||
"values": Array [],
|
||||
}
|
||||
}
|
||||
url="/"
|
||||
>
|
||||
<EuiCollapsibleNav
|
||||
aria-label="Primary"
|
||||
|
@ -2511,6 +2522,7 @@ exports[`CollapsibleNav renders the default nav 2`] = `
|
|||
"values": Array [],
|
||||
}
|
||||
}
|
||||
url="/"
|
||||
>
|
||||
<EuiCollapsibleNav
|
||||
aria-label="Primary"
|
||||
|
@ -2748,6 +2760,7 @@ exports[`CollapsibleNav renders the default nav 3`] = `
|
|||
"values": Array [],
|
||||
}
|
||||
}
|
||||
url="/"
|
||||
>
|
||||
<EuiCollapsibleNav
|
||||
aria-label="Primary"
|
||||
|
|
|
@ -357,6 +357,7 @@ exports[`Header renders 1`] = `
|
|||
"href": "",
|
||||
"id": "cloud-deployment-link",
|
||||
"title": "Manage cloud deployment",
|
||||
"url": "",
|
||||
},
|
||||
"closed": false,
|
||||
"hasError": false,
|
||||
|
@ -1607,6 +1608,7 @@ exports[`Header renders 1`] = `
|
|||
"href": "",
|
||||
"id": "kibana",
|
||||
"title": "kibana",
|
||||
"url": "",
|
||||
},
|
||||
],
|
||||
"closed": false,
|
||||
|
@ -1924,6 +1926,7 @@ exports[`Header renders 1`] = `
|
|||
"href": "",
|
||||
"id": "kibana",
|
||||
"title": "kibana",
|
||||
"url": "",
|
||||
},
|
||||
],
|
||||
"closed": false,
|
||||
|
@ -3027,6 +3030,7 @@ exports[`Header renders 1`] = `
|
|||
"href": "",
|
||||
"id": "kibana",
|
||||
"title": "kibana",
|
||||
"url": "",
|
||||
},
|
||||
],
|
||||
"closed": false,
|
||||
|
@ -4731,6 +4735,7 @@ exports[`Header renders 1`] = `
|
|||
"href": "",
|
||||
"id": "cloud-deployment-link",
|
||||
"title": "Manage cloud deployment",
|
||||
"url": "",
|
||||
},
|
||||
"closed": false,
|
||||
"hasError": false,
|
||||
|
@ -4790,6 +4795,7 @@ exports[`Header renders 1`] = `
|
|||
"href": "",
|
||||
"id": "kibana",
|
||||
"title": "kibana",
|
||||
"url": "",
|
||||
},
|
||||
],
|
||||
"closed": false,
|
||||
|
|
|
@ -29,6 +29,7 @@ function mockLink({ title = 'discover', category }: Partial<ChromeNavLink>) {
|
|||
id: title,
|
||||
href: title,
|
||||
baseUrl: '/',
|
||||
url: '/',
|
||||
isActive: true,
|
||||
'data-test-subj': title,
|
||||
};
|
||||
|
@ -50,6 +51,7 @@ function mockProps() {
|
|||
isLocked: false,
|
||||
isNavOpen: false,
|
||||
homeHref: '/',
|
||||
url: '/',
|
||||
navLinks$: new BehaviorSubject([]),
|
||||
recentlyAccessed$: new BehaviorSubject([]),
|
||||
storage: new StubBrowserStorage(),
|
||||
|
|
|
@ -107,7 +107,7 @@ export function CollapsibleNav({
|
|||
link,
|
||||
appId,
|
||||
dataTestSubj: 'collapsibleNavAppLink',
|
||||
navigateToApp,
|
||||
navigateToUrl,
|
||||
onClick: closeNav,
|
||||
...(needsIcon && { basePath }),
|
||||
});
|
||||
|
@ -137,7 +137,7 @@ export function CollapsibleNav({
|
|||
createEuiListItem({
|
||||
link: customNavLink,
|
||||
basePath,
|
||||
navigateToApp,
|
||||
navigateToUrl,
|
||||
dataTestSubj: 'collapsibleNavCustomNavLink',
|
||||
onClick: closeNav,
|
||||
externalLink: true,
|
||||
|
|
|
@ -61,13 +61,14 @@ describe('Header', () => {
|
|||
const breadcrumbs$ = new BehaviorSubject([{ text: 'test' }]);
|
||||
const isLocked$ = new BehaviorSubject(false);
|
||||
const navLinks$ = new BehaviorSubject([
|
||||
{ id: 'kibana', title: 'kibana', baseUrl: '', href: '' },
|
||||
{ id: 'kibana', title: 'kibana', baseUrl: '', href: '', url: '' },
|
||||
]);
|
||||
const headerBanner$ = new BehaviorSubject(undefined);
|
||||
const customNavLink$ = new BehaviorSubject({
|
||||
id: 'cloud-deployment-link',
|
||||
title: 'Manage cloud deployment',
|
||||
baseUrl: '',
|
||||
url: '',
|
||||
href: '',
|
||||
});
|
||||
const recentlyAccessed$ = new BehaviorSubject([
|
||||
|
|
|
@ -23,7 +23,7 @@ interface Props {
|
|||
basePath?: HttpStart['basePath'];
|
||||
dataTestSubj: string;
|
||||
onClick?: Function;
|
||||
navigateToApp: CoreStart['application']['navigateToApp'];
|
||||
navigateToUrl: CoreStart['application']['navigateToUrl'];
|
||||
externalLink?: boolean;
|
||||
}
|
||||
|
||||
|
@ -36,11 +36,11 @@ export function createEuiListItem({
|
|||
appId,
|
||||
basePath,
|
||||
onClick = () => {},
|
||||
navigateToApp,
|
||||
navigateToUrl,
|
||||
dataTestSubj,
|
||||
externalLink = false,
|
||||
}: Props) {
|
||||
const { href, id, title, disabled, euiIconType, icon, tooltip } = link;
|
||||
const { href, id, title, disabled, euiIconType, icon, tooltip, url } = link;
|
||||
|
||||
return {
|
||||
label: tooltip ?? title,
|
||||
|
@ -57,7 +57,7 @@ export function createEuiListItem({
|
|||
!isModifiedOrPrevented(event)
|
||||
) {
|
||||
event.preventDefault();
|
||||
navigateToApp(id);
|
||||
navigateToUrl(url);
|
||||
}
|
||||
},
|
||||
isActive: appId === id,
|
||||
|
|
|
@ -91,6 +91,7 @@ export type {
|
|||
AppLeaveConfirmAction,
|
||||
AppUpdatableFields,
|
||||
AppUpdater,
|
||||
AppNavOptions,
|
||||
AppDeepLink,
|
||||
PublicAppInfo,
|
||||
PublicAppDeepLinkInfo,
|
||||
|
|
|
@ -53,24 +53,21 @@ import { UserProvidedValues as UserProvidedValues_2 } from 'src/core/server/type
|
|||
export function __kbnBootstrap__(): Promise<void>;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface App<HistoryLocationState = unknown> {
|
||||
export interface App<HistoryLocationState = unknown> extends AppNavOptions {
|
||||
appRoute?: string;
|
||||
capabilities?: Partial<Capabilities>;
|
||||
category?: AppCategory;
|
||||
chromeless?: boolean;
|
||||
deepLinks?: AppDeepLink[];
|
||||
defaultPath?: string;
|
||||
euiIconType?: string;
|
||||
exactRoute?: boolean;
|
||||
icon?: string;
|
||||
id: string;
|
||||
keywords?: string[];
|
||||
mount: AppMount<HistoryLocationState>;
|
||||
navLinkStatus?: AppNavLinkStatus;
|
||||
order?: number;
|
||||
searchable?: boolean;
|
||||
status?: AppStatus;
|
||||
title: string;
|
||||
tooltip?: string;
|
||||
updater$?: Observable<AppUpdater>;
|
||||
}
|
||||
|
||||
|
@ -92,7 +89,8 @@ export type AppDeepLink = {
|
|||
title: string;
|
||||
keywords?: string[];
|
||||
navLinkStatus?: AppNavLinkStatus;
|
||||
} & ({
|
||||
searchable?: boolean;
|
||||
} & AppNavOptions & ({
|
||||
path: string;
|
||||
deepLinks?: AppDeepLink[];
|
||||
} | {
|
||||
|
@ -179,6 +177,14 @@ export enum AppNavLinkStatus {
|
|||
visible = 1
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface AppNavOptions {
|
||||
euiIconType?: string;
|
||||
icon?: string;
|
||||
order?: number;
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
export enum AppStatus {
|
||||
accessible = 0,
|
||||
|
@ -189,7 +195,7 @@ export enum AppStatus {
|
|||
export type AppUnmount = () => void;
|
||||
|
||||
// @public
|
||||
export type AppUpdatableFields = Pick<App, 'status' | 'navLinkStatus' | 'tooltip' | 'defaultPath' | 'deepLinks'>;
|
||||
export type AppUpdatableFields = Pick<App, 'status' | 'navLinkStatus' | 'searchable' | 'tooltip' | 'defaultPath' | 'deepLinks'>;
|
||||
|
||||
// @public
|
||||
export type AppUpdater = (app: App) => Partial<AppUpdatableFields> | undefined;
|
||||
|
@ -314,8 +320,7 @@ export interface ChromeNavLink {
|
|||
readonly order?: number;
|
||||
readonly title: string;
|
||||
readonly tooltip?: string;
|
||||
// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "AppBase"
|
||||
readonly url?: string;
|
||||
readonly url: string;
|
||||
}
|
||||
|
||||
// @public
|
||||
|
@ -916,10 +921,9 @@ export interface IUiSettingsClient {
|
|||
// @public
|
||||
export type MountPoint<T extends HTMLElement = HTMLElement> = (element: T) => UnmountCallback;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "NavigateToAppOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public
|
||||
export interface NavigateToAppOptions {
|
||||
deepLinkId?: string;
|
||||
openInNewTab?: boolean;
|
||||
path?: string;
|
||||
replace?: boolean;
|
||||
|
@ -1067,19 +1071,21 @@ export interface PluginInitializerContext<ConfigSchema extends object = object>
|
|||
export type PluginOpaqueId = symbol;
|
||||
|
||||
// @public
|
||||
export type PublicAppDeepLinkInfo = Omit<AppDeepLink, 'deepLinks' | 'keywords' | 'navLinkStatus'> & {
|
||||
export type PublicAppDeepLinkInfo = Omit<AppDeepLink, 'deepLinks' | 'keywords' | 'navLinkStatus' | 'searchable'> & {
|
||||
deepLinks: PublicAppDeepLinkInfo[];
|
||||
keywords: string[];
|
||||
navLinkStatus: AppNavLinkStatus;
|
||||
searchable: boolean;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type PublicAppInfo = Omit<App, 'mount' | 'updater$' | 'keywords' | 'deepLinks'> & {
|
||||
export type PublicAppInfo = Omit<App, 'mount' | 'updater$' | 'keywords' | 'deepLinks' | 'searchable'> & {
|
||||
status: AppStatus;
|
||||
navLinkStatus: AppNavLinkStatus;
|
||||
appRoute: string;
|
||||
keywords: string[];
|
||||
deepLinks: PublicAppDeepLinkInfo[];
|
||||
searchable: boolean;
|
||||
};
|
||||
|
||||
// @public
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"id": "corePluginDeepLinks",
|
||||
"version": "0.0.1",
|
||||
"kibanaVersion": "kibana",
|
||||
"configPath": ["core_plugin_deep_links"],
|
||||
"ui": true
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"name": "core_plugin_deep_links",
|
||||
"version": "1.0.0",
|
||||
"main": "target/test/plugin_functional/plugins/core_plugin_deep_links",
|
||||
"kibana": {
|
||||
"version": "kibana",
|
||||
"templateVersion": "1.0.0"
|
||||
},
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0",
|
||||
"scripts": {
|
||||
"kbn": "node ../../../../scripts/kbn.js",
|
||||
"build": "rm -rf './target' && ../../../../node_modules/.bin/tsc"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { History } from 'history';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Router, Route, withRouter, RouteComponentProps, Redirect } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
EuiPage,
|
||||
EuiPageBody,
|
||||
EuiPageContent,
|
||||
EuiPageContentBody,
|
||||
EuiPageContentHeader,
|
||||
EuiPageContentHeaderSection,
|
||||
EuiPageHeader,
|
||||
EuiPageHeaderSection,
|
||||
EuiPageSideBar,
|
||||
EuiTitle,
|
||||
EuiSideNav,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { CoreStart, AppMountParameters } from 'kibana/public';
|
||||
|
||||
const Home = () => (
|
||||
<EuiPageBody data-test-subj="dlAppHome">
|
||||
<EuiPageHeader>
|
||||
<EuiPageHeaderSection>
|
||||
<EuiTitle size="l">
|
||||
<h1>Welcome to DL!</h1>
|
||||
</EuiTitle>
|
||||
</EuiPageHeaderSection>
|
||||
</EuiPageHeader>
|
||||
<EuiPageContent>
|
||||
<EuiPageContentHeader>
|
||||
<EuiPageContentHeaderSection>
|
||||
<EuiTitle>
|
||||
<h2>DL home page section title</h2>
|
||||
</EuiTitle>
|
||||
</EuiPageContentHeaderSection>
|
||||
</EuiPageContentHeader>
|
||||
<EuiPageContentBody>Wow this is the content!</EuiPageContentBody>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
);
|
||||
|
||||
const PageA = () => (
|
||||
<EuiPageBody data-test-subj="dlAppPageA">
|
||||
<EuiPageHeader>
|
||||
<EuiPageHeaderSection>
|
||||
<EuiTitle size="l">
|
||||
<h1>DL Page A</h1>
|
||||
</EuiTitle>
|
||||
</EuiPageHeaderSection>
|
||||
</EuiPageHeader>
|
||||
<EuiPageContent>
|
||||
<EuiPageContentHeader>
|
||||
<EuiPageContentHeaderSection>
|
||||
<EuiTitle>
|
||||
<h2>DL Page A section title</h2>
|
||||
</EuiTitle>
|
||||
</EuiPageContentHeaderSection>
|
||||
</EuiPageContentHeader>
|
||||
<EuiPageContentBody>DL Page A's content goes here</EuiPageContentBody>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
);
|
||||
|
||||
const PageB = () => (
|
||||
<EuiPageBody data-test-subj="dlAppPageB">
|
||||
<EuiPageHeader>
|
||||
<EuiPageHeaderSection>
|
||||
<EuiTitle size="l">
|
||||
<h1>DL Page B</h1>
|
||||
</EuiTitle>
|
||||
</EuiPageHeaderSection>
|
||||
</EuiPageHeader>
|
||||
<EuiPageContent>
|
||||
<EuiPageContentHeader>
|
||||
<EuiPageContentHeaderSection>
|
||||
<EuiTitle>
|
||||
<h2>DL Page B section title</h2>
|
||||
</EuiTitle>
|
||||
</EuiPageContentHeaderSection>
|
||||
</EuiPageContentHeader>
|
||||
<EuiPageContentBody>DL Page B's content goes here</EuiPageContentBody>
|
||||
</EuiPageContent>
|
||||
</EuiPageBody>
|
||||
);
|
||||
|
||||
type NavProps = RouteComponentProps & {
|
||||
navigateToApp: CoreStart['application']['navigateToApp'];
|
||||
};
|
||||
const Nav = withRouter(({ history, navigateToApp }: NavProps) => (
|
||||
<EuiSideNav
|
||||
items={[
|
||||
{
|
||||
name: 'DeepLinks!',
|
||||
id: 'deeplinks',
|
||||
items: [
|
||||
{
|
||||
id: 'home',
|
||||
name: 'DL Home',
|
||||
onClick: () => history.push('/home'),
|
||||
'data-test-subj': 'dlNavHome',
|
||||
},
|
||||
{
|
||||
id: 'page-a',
|
||||
name: 'DL page A',
|
||||
onClick: () => history.push('/page-a'),
|
||||
'data-test-subj': 'dlNavPageA',
|
||||
},
|
||||
{
|
||||
id: 'navigateDeepByPath',
|
||||
name: 'DL section 1 page B',
|
||||
onClick: () => {
|
||||
navigateToApp('deeplinks', { path: '/page-b' });
|
||||
},
|
||||
'data-test-subj': 'dlNavDeepPageB',
|
||||
},
|
||||
{
|
||||
id: 'navigateDeepById',
|
||||
name: 'DL page A deep link',
|
||||
onClick: () => {
|
||||
navigateToApp('deeplinks', { deepLinkId: 'pageA' });
|
||||
},
|
||||
'data-test-subj': 'dlNavDeepPageAById',
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
));
|
||||
|
||||
const DlApp = ({ history, coreStart }: { history: History; coreStart: CoreStart }) => (
|
||||
<Router history={history}>
|
||||
<EuiPage>
|
||||
<EuiPageSideBar>
|
||||
<Nav navigateToApp={coreStart.application.navigateToApp} />
|
||||
</EuiPageSideBar>
|
||||
<Route path="/" exact render={() => <Redirect to="/home" />} />
|
||||
<Route path="/home" exact component={Home} />
|
||||
<Route path="/page-a" component={PageA} />
|
||||
<Route path="/page-b" component={PageB} />
|
||||
</EuiPage>
|
||||
</Router>
|
||||
);
|
||||
|
||||
export const renderApp = (coreStart: CoreStart, { history, element }: AppMountParameters) => {
|
||||
ReactDOM.render(<DlApp history={history} coreStart={coreStart} />, element);
|
||||
|
||||
return () => ReactDOM.unmountComponentAtNode(element);
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { PluginInitializer } from 'kibana/public';
|
||||
import {
|
||||
CorePluginDeepLinksPlugin,
|
||||
CorePluginDeepLinksPluginSetup,
|
||||
CorePluginDeepLinksPluginStart,
|
||||
} from './plugin';
|
||||
|
||||
export const plugin: PluginInitializer<
|
||||
CorePluginDeepLinksPluginSetup,
|
||||
CorePluginDeepLinksPluginStart
|
||||
> = () => new CorePluginDeepLinksPlugin();
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Plugin, CoreSetup } from 'kibana/public';
|
||||
import { DEFAULT_APP_CATEGORIES, AppNavLinkStatus } from '../../../../../src/core/public';
|
||||
|
||||
export class CorePluginDeepLinksPlugin
|
||||
implements Plugin<CorePluginDeepLinksPluginSetup, CorePluginDeepLinksPluginStart> {
|
||||
public setup(core: CoreSetup, deps: {}) {
|
||||
core.application.register({
|
||||
id: 'deeplinks',
|
||||
title: 'Deep Links',
|
||||
appRoute: '/app/dl',
|
||||
defaultPath: '/home',
|
||||
category: DEFAULT_APP_CATEGORIES.security,
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
deepLinks: [
|
||||
{
|
||||
id: 'home',
|
||||
title: 'DL Home',
|
||||
path: '/home',
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
},
|
||||
{
|
||||
id: 'pageA',
|
||||
title: 'DL Page A',
|
||||
path: '/page-a',
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
},
|
||||
{
|
||||
id: 'sectionOne',
|
||||
title: 'DL Section One',
|
||||
deepLinks: [
|
||||
{
|
||||
id: 'pageB',
|
||||
title: 'DL Page B',
|
||||
path: '/page-b',
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'pageC',
|
||||
title: 'DL Page C',
|
||||
path: '/page-c',
|
||||
// navLinkStatus hidden by default
|
||||
},
|
||||
],
|
||||
async mount(params) {
|
||||
const { renderApp } = await import('./application');
|
||||
const [coreStart] = await core.getStartServices();
|
||||
return renderApp(coreStart, params);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
getGreeting() {
|
||||
return 'Hello from Deep Link Plugin!';
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public start() {}
|
||||
public stop() {}
|
||||
}
|
||||
|
||||
export type CorePluginDeepLinksPluginSetup = ReturnType<CorePluginDeepLinksPlugin['setup']>;
|
||||
export type CorePluginDeepLinksPluginStart = ReturnType<CorePluginDeepLinksPlugin['start']>;
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./target",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"index.ts",
|
||||
"public/**/*.ts",
|
||||
"public/**/*.tsx",
|
||||
"../../../../typings/**/*",
|
||||
],
|
||||
"exclude": [],
|
||||
"references": [
|
||||
{ "path": "../../../../src/core/tsconfig.json" }
|
||||
]
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import url from 'url';
|
||||
import expect from '@kbn/expect';
|
||||
import { PluginFunctionalProviderContext } from '../../services';
|
||||
|
||||
export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) {
|
||||
const PageObjects = getPageObjects(['common']);
|
||||
|
||||
const browser = getService('browser');
|
||||
const appsMenu = getService('appsMenu');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const retry = getService('retry');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
const loadingScreenNotShown = async () =>
|
||||
expect(await testSubjects.exists('kbnLoadingMessage')).to.be(false);
|
||||
|
||||
const getKibanaUrl = (pathname?: string, search?: string) =>
|
||||
url.format({
|
||||
protocol: 'http:',
|
||||
hostname: process.env.TEST_KIBANA_HOST || 'localhost',
|
||||
port: process.env.TEST_KIBANA_PORT || '5620',
|
||||
pathname,
|
||||
search,
|
||||
});
|
||||
|
||||
/** Use retry logic to make URL assertions less flaky */
|
||||
const waitForUrlToBe = (pathname?: string, search?: string) => {
|
||||
const expectedUrl = getKibanaUrl(pathname, search);
|
||||
return retry.waitFor(`Url to be ${expectedUrl}`, async () => {
|
||||
return (await browser.getCurrentUrl()) === expectedUrl;
|
||||
});
|
||||
};
|
||||
|
||||
describe('application deep links navigation', function describeDeepLinksTests() {
|
||||
before(async () => {
|
||||
await esArchiver.emptyKibanaIndex();
|
||||
await PageObjects.common.navigateToApp('dl');
|
||||
});
|
||||
|
||||
it('should start on home page', async () => {
|
||||
await testSubjects.existOrFail('dlAppHome');
|
||||
});
|
||||
|
||||
it('should navigate to page A when navlink is clicked', async () => {
|
||||
await appsMenu.clickLink('DL Page A');
|
||||
await waitForUrlToBe('/app/dl/page-a');
|
||||
await loadingScreenNotShown();
|
||||
await testSubjects.existOrFail('dlAppPageA');
|
||||
});
|
||||
|
||||
it('should be able to use the back button to navigate back to previous deep link', async () => {
|
||||
await browser.goBack();
|
||||
await waitForUrlToBe('/app/dl/home');
|
||||
await loadingScreenNotShown();
|
||||
await testSubjects.existOrFail('dlAppHome');
|
||||
});
|
||||
|
||||
it('should navigate to nested page B when navlink is clicked', async () => {
|
||||
await appsMenu.clickLink('DL Page B');
|
||||
await waitForUrlToBe('/app/dl/page-b');
|
||||
await loadingScreenNotShown();
|
||||
await testSubjects.existOrFail('dlAppPageB');
|
||||
});
|
||||
|
||||
it('should navigate to Home when navlink is clicked inside the defined category group', async () => {
|
||||
await appsMenu.clickLink('DL Home', { category: 'securitySolution' });
|
||||
await waitForUrlToBe('/app/dl/home');
|
||||
await loadingScreenNotShown();
|
||||
await testSubjects.existOrFail('dlAppHome');
|
||||
});
|
||||
|
||||
it('should navigate to nested page B using navigateToApp path', async () => {
|
||||
await testSubjects.click('dlNavDeepPageB');
|
||||
await waitForUrlToBe('/app/dl/page-b');
|
||||
await loadingScreenNotShown();
|
||||
await testSubjects.existOrFail('dlAppPageB');
|
||||
});
|
||||
|
||||
it('should navigate to nested page A using navigateToApp deepLinkId', async () => {
|
||||
await testSubjects.click('dlNavDeepPageAById');
|
||||
await waitForUrlToBe('/app/dl/page-a');
|
||||
await loadingScreenNotShown();
|
||||
await testSubjects.existOrFail('dlAppPageA');
|
||||
});
|
||||
|
||||
it('should not display hidden deep links', async () => {
|
||||
expect(await appsMenu.linkExists('DL Section One')).to.be(false);
|
||||
expect(await appsMenu.linkExists('DL Page C')).to.be(false);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -18,6 +18,7 @@ export default function ({ loadTestFile }: PluginFunctionalProviderContext) {
|
|||
loadTestFile(require.resolve('./top_nav'));
|
||||
loadTestFile(require.resolve('./application_leave_confirm'));
|
||||
loadTestFile(require.resolve('./application_status'));
|
||||
loadTestFile(require.resolve('./application_deep_links'));
|
||||
loadTestFile(require.resolve('./rendering'));
|
||||
loadTestFile(require.resolve('./chrome_help_menu_links'));
|
||||
loadTestFile(require.resolve('./history_block'));
|
||||
|
|
|
@ -28,6 +28,7 @@ const createApp = (props: Partial<PublicAppInfo> = {}): PublicAppInfo => ({
|
|||
appRoute: '/app/app1',
|
||||
status: AppStatus.accessible,
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
searchable: true,
|
||||
chromeless: false,
|
||||
keywords: props.keywords || [],
|
||||
deepLinks: [],
|
||||
|
@ -163,7 +164,7 @@ describe('applicationResultProvider', () => {
|
|||
expect(getAppResultsMock).toHaveBeenCalledWith('term', [expectApp('app1')]);
|
||||
});
|
||||
|
||||
it('ignores apps with non-visible navlink', async () => {
|
||||
it('does not ignore apps with non-visible navlink', async () => {
|
||||
application.applications$ = of(
|
||||
createAppMap([
|
||||
createApp({ id: 'app1', title: 'App 1', navLinkStatus: AppNavLinkStatus.visible }),
|
||||
|
@ -178,7 +179,11 @@ describe('applicationResultProvider', () => {
|
|||
const provider = createApplicationResultProvider(Promise.resolve(application));
|
||||
await provider.find({ term: 'term' }, defaultOption).toPromise();
|
||||
|
||||
expect(getAppResultsMock).toHaveBeenCalledWith('term', [expectApp('app1')]);
|
||||
expect(getAppResultsMock).toHaveBeenCalledWith('term', [
|
||||
expectApp('app1'),
|
||||
expectApp('disabled'),
|
||||
expectApp('hidden'),
|
||||
]);
|
||||
});
|
||||
|
||||
it('ignores chromeless apps', async () => {
|
||||
|
|
|
@ -20,8 +20,8 @@ export const createApplicationResultProvider = (
|
|||
mergeMap((application) => application.applications$),
|
||||
map((apps) =>
|
||||
[...apps.values()].filter(
|
||||
// only include non-chromeless enabled apps with visible navLinks
|
||||
(app) => app.status === 0 && app.navLinkStatus === 1 && app.chromeless !== true
|
||||
// only include non-chromeless enabled apps
|
||||
(app) => app.status === 0 && app.chromeless !== true
|
||||
)
|
||||
),
|
||||
shareReplay(1)
|
||||
|
|
|
@ -25,6 +25,7 @@ const createApp = (props: Partial<PublicAppInfo> = {}): PublicAppInfo => ({
|
|||
appRoute: '/app/app1',
|
||||
status: AppStatus.accessible,
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
searchable: true,
|
||||
chromeless: false,
|
||||
keywords: [],
|
||||
deepLinks: [],
|
||||
|
@ -44,6 +45,11 @@ describe('getAppResults', () => {
|
|||
const apps = [
|
||||
createApp({ id: 'dashboard', title: 'dashboard' }),
|
||||
createApp({ id: 'visualize', title: 'visualize' }),
|
||||
createApp({
|
||||
id: 'dashboard_not_searchable',
|
||||
title: 'dashboard not searchable',
|
||||
searchable: false,
|
||||
}),
|
||||
];
|
||||
|
||||
const results = getAppResults('dashboard', apps);
|
||||
|
@ -63,6 +69,7 @@ describe('getAppResults', () => {
|
|||
deepLinks: [],
|
||||
keywords: [],
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
id: 'sub2',
|
||||
|
@ -76,10 +83,12 @@ describe('getAppResults', () => {
|
|||
deepLinks: [],
|
||||
keywords: [],
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
searchable: true,
|
||||
},
|
||||
],
|
||||
keywords: [],
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
navLinkStatus: AppNavLinkStatus.visible,
|
||||
searchable: false,
|
||||
},
|
||||
],
|
||||
keywords: [],
|
||||
|
@ -88,11 +97,9 @@ describe('getAppResults', () => {
|
|||
|
||||
const results = getAppResults('App 1', apps);
|
||||
|
||||
expect(results.length).toBe(4);
|
||||
expect(results.map(({ title }) => title)).toEqual([
|
||||
'App 1',
|
||||
'App 1 / Sub1',
|
||||
'App 1 / Sub2',
|
||||
'App 1 / Sub2 / Sub2Sub1',
|
||||
]);
|
||||
});
|
||||
|
@ -108,6 +115,7 @@ describe('getAppResults', () => {
|
|||
deepLinks: [],
|
||||
keywords: [],
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
searchable: true,
|
||||
},
|
||||
],
|
||||
keywords: [],
|
||||
|
@ -135,6 +143,7 @@ describe('getAppResults', () => {
|
|||
deepLinks: [],
|
||||
keywords: [],
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
id: 'sub2',
|
||||
|
@ -148,10 +157,12 @@ describe('getAppResults', () => {
|
|||
deepLinks: [],
|
||||
keywords: ['TwoOne'],
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
searchable: true,
|
||||
},
|
||||
],
|
||||
keywords: ['two'],
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
searchable: true,
|
||||
},
|
||||
],
|
||||
keywords: [],
|
||||
|
|
|
@ -118,18 +118,22 @@ export const appToResult = (appLink: AppLink, score: number): GlobalSearchProvid
|
|||
const flattenDeepLinks = (app: PublicAppInfo, deepLink?: PublicAppDeepLinkInfo): AppLink[] => {
|
||||
if (!deepLink) {
|
||||
return [
|
||||
{
|
||||
id: app.id,
|
||||
app,
|
||||
path: app.appRoute,
|
||||
subLinkTitles: [],
|
||||
keywords: app?.keywords ?? [],
|
||||
},
|
||||
...(app.searchable
|
||||
? [
|
||||
{
|
||||
id: app.id,
|
||||
app,
|
||||
path: app.appRoute,
|
||||
subLinkTitles: [],
|
||||
keywords: app?.keywords ?? [],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...app.deepLinks.flatMap((appDeepLink) => flattenDeepLinks(app, appDeepLink)),
|
||||
];
|
||||
}
|
||||
return [
|
||||
...(deepLink.path
|
||||
...(deepLink.path && deepLink.searchable
|
||||
? [
|
||||
{
|
||||
id: `${app.id}-${deepLink.id}`,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue