Move application.applications$ to public contract (#67463)

* expose applications$ on public contract

* review comments
This commit is contained in:
Pierre Gayvallet 2020-06-02 15:15:21 +02:00 committed by GitHub
parent 7d0ffb53bc
commit a091124fab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 286 additions and 35 deletions

View file

@ -0,0 +1,18 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [ApplicationStart](./kibana-plugin-core-public.applicationstart.md) &gt; [applications$](./kibana-plugin-core-public.applicationstart.applications_.md)
## ApplicationStart.applications$ property
Observable emitting the list of currently registered apps and their associated status.
<b>Signature:</b>
```typescript
applications$: Observable<ReadonlyMap<string, PublicAppInfo | PublicLegacyAppInfo>>;
```
## Remarks
Applications disabled by [Capabilities](./kibana-plugin-core-public.capabilities.md) will not be present in the map. Applications manually disabled from the client-side using an [application updater](./kibana-plugin-core-public.appupdater.md) are present, with their status properly set as `inaccessible`<!-- -->.

View file

@ -15,6 +15,7 @@ export interface ApplicationStart
| Property | Type | Description |
| --- | --- | --- |
| [applications$](./kibana-plugin-core-public.applicationstart.applications_.md) | <code>Observable&lt;ReadonlyMap&lt;string, PublicAppInfo &#124; PublicLegacyAppInfo&gt;&gt;</code> | Observable emitting the list of currently registered apps and their associated status. |
| [capabilities](./kibana-plugin-core-public.applicationstart.capabilities.md) | <code>RecursiveReadonly&lt;Capabilities&gt;</code> | Gets the read-only capabilities. |
| [currentAppId$](./kibana-plugin-core-public.applicationstart.currentappid_.md) | <code>Observable&lt;string &#124; undefined&gt;</code> | An observable that emits the current application id and each subsequent id update. |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [LegacyApp](./kibana-plugin-core-public.legacyapp.md) &gt; [appUrl](./kibana-plugin-core-public.legacyapp.appurl.md)
## LegacyApp.appUrl property
<b>Signature:</b>
```typescript
appUrl: string;
```

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [LegacyApp](./kibana-plugin-core-public.legacyapp.md) &gt; [disableSubUrlTracking](./kibana-plugin-core-public.legacyapp.disablesuburltracking.md)
## LegacyApp.disableSubUrlTracking property
<b>Signature:</b>
```typescript
disableSubUrlTracking?: boolean;
```

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [LegacyApp](./kibana-plugin-core-public.legacyapp.md) &gt; [linkToLastSubUrl](./kibana-plugin-core-public.legacyapp.linktolastsuburl.md)
## LegacyApp.linkToLastSubUrl property
<b>Signature:</b>
```typescript
linkToLastSubUrl?: boolean;
```

View file

@ -0,0 +1,22 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [LegacyApp](./kibana-plugin-core-public.legacyapp.md)
## LegacyApp interface
<b>Signature:</b>
```typescript
export interface LegacyApp extends AppBase
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [appUrl](./kibana-plugin-core-public.legacyapp.appurl.md) | <code>string</code> | |
| [disableSubUrlTracking](./kibana-plugin-core-public.legacyapp.disablesuburltracking.md) | <code>boolean</code> | |
| [linkToLastSubUrl](./kibana-plugin-core-public.legacyapp.linktolastsuburl.md) | <code>boolean</code> | |
| [subUrlBase](./kibana-plugin-core-public.legacyapp.suburlbase.md) | <code>string</code> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [LegacyApp](./kibana-plugin-core-public.legacyapp.md) &gt; [subUrlBase](./kibana-plugin-core-public.legacyapp.suburlbase.md)
## LegacyApp.subUrlBase property
<b>Signature:</b>
```typescript
subUrlBase?: string;
```

View file

@ -90,6 +90,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [IHttpResponseInterceptorOverrides](./kibana-plugin-core-public.ihttpresponseinterceptoroverrides.md) | Properties that can be returned by HttpInterceptor.request to override the response. |
| [ImageValidation](./kibana-plugin-core-public.imagevalidation.md) | |
| [IUiSettingsClient](./kibana-plugin-core-public.iuisettingsclient.md) | Client-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. [IUiSettingsClient](./kibana-plugin-core-public.iuisettingsclient.md) |
| [LegacyApp](./kibana-plugin-core-public.legacyapp.md) | |
| [LegacyCoreSetup](./kibana-plugin-core-public.legacycoresetup.md) | Setup interface exposed to the legacy platform via the <code>ui/new_platform</code> module. |
| [LegacyCoreStart](./kibana-plugin-core-public.legacycorestart.md) | Start interface exposed to the legacy platform via the <code>ui/new_platform</code> module. |
| [LegacyNavLink](./kibana-plugin-core-public.legacynavlink.md) | |
@ -162,6 +163,8 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [NavType](./kibana-plugin-core-public.navtype.md) | |
| [PluginInitializer](./kibana-plugin-core-public.plugininitializer.md) | The <code>plugin</code> export at the root of a plugin's <code>public</code> directory should conform to this interface. |
| [PluginOpaqueId](./kibana-plugin-core-public.pluginopaqueid.md) | |
| [PublicAppInfo](./kibana-plugin-core-public.publicappinfo.md) | Public information about a registered [application](./kibana-plugin-core-public.app.md) |
| [PublicLegacyAppInfo](./kibana-plugin-core-public.publiclegacyappinfo.md) | Information about a registered [legacy application](./kibana-plugin-core-public.legacyapp.md) |
| [PublicUiSettingsParams](./kibana-plugin-core-public.publicuisettingsparams.md) | A sub-set of [UiSettingsParams](./kibana-plugin-core-public.uisettingsparams.md) exposed to the client-side. |
| [RecursiveReadonly](./kibana-plugin-core-public.recursivereadonly.md) | |
| [SavedObjectAttribute](./kibana-plugin-core-public.savedobjectattribute.md) | Type definition for a Saved Object attribute value |

View file

@ -0,0 +1,15 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [PublicAppInfo](./kibana-plugin-core-public.publicappinfo.md)
## PublicAppInfo type
Public information about a registered [application](./kibana-plugin-core-public.app.md)
<b>Signature:</b>
```typescript
export declare type PublicAppInfo = Omit<App, 'mount' | 'updater$'> & {
legacy: false;
};
```

View file

@ -0,0 +1,15 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-core-public](./kibana-plugin-core-public.md) &gt; [PublicLegacyAppInfo](./kibana-plugin-core-public.publiclegacyappinfo.md)
## PublicLegacyAppInfo type
Information about a registered [legacy application](./kibana-plugin-core-public.legacyapp.md)
<b>Signature:</b>
```typescript
export declare type PublicLegacyAppInfo = Omit<LegacyApp, 'updater$'> & {
legacy: true;
};
```

View file

@ -25,8 +25,8 @@ import {
InternalApplicationStart,
ApplicationStart,
InternalApplicationSetup,
App,
LegacyApp,
PublicAppInfo,
PublicLegacyAppInfo,
} from './types';
import { ApplicationServiceContract } from './test_types';
@ -47,6 +47,7 @@ const createStartContractMock = (): jest.Mocked<ApplicationStart> => {
const currentAppId$ = new Subject<string | undefined>();
return {
applications$: new BehaviorSubject<Map<string, PublicAppInfo | PublicLegacyAppInfo>>(new Map()),
currentAppId$: currentAppId$.asObservable(),
capabilities: capabilitiesServiceMock.createStartContract().capabilities,
navigateToApp: jest.fn(),
@ -60,7 +61,7 @@ const createInternalStartContractMock = (): jest.Mocked<InternalApplicationStart
const currentAppId$ = new Subject<string | undefined>();
return {
applications$: new BehaviorSubject<Map<string, App | LegacyApp>>(new Map()),
applications$: new BehaviorSubject<Map<string, PublicAppInfo | PublicLegacyAppInfo>>(new Map()),
capabilities: capabilitiesServiceMock.createStartContract().capabilities,
currentAppId$: currentAppId$.asObservable(),
getComponent: jest.fn(),

View file

@ -34,7 +34,15 @@ import { httpServiceMock } from '../http/http_service.mock';
import { overlayServiceMock } from '../overlays/overlay_service.mock';
import { MockLifecycle } from './test_types';
import { ApplicationService } from './application_service';
import { App, AppNavLinkStatus, AppStatus, AppUpdater, LegacyApp } from './types';
import {
App,
PublicAppInfo,
AppNavLinkStatus,
AppStatus,
AppUpdater,
LegacyApp,
PublicLegacyAppInfo,
} from './types';
import { act } from 'react-dom/test-utils';
const createApp = (props: Partial<App>): App => {
@ -366,7 +374,10 @@ describe('#setup()', () => {
setup.registerAppUpdater(statusUpdater);
const start = await service.start(startDeps);
let latestValue: ReadonlyMap<string, App | LegacyApp> = new Map<string, App | LegacyApp>();
let latestValue: ReadonlyMap<string, PublicAppInfo | PublicLegacyAppInfo> = new Map<
string,
PublicAppInfo | PublicLegacyAppInfo
>();
start.applications$.subscribe((apps) => {
latestValue = apps;
});

View file

@ -46,7 +46,7 @@ import {
Mounter,
} from './types';
import { getLeaveAction, isConfirmAction } from './application_leave';
import { appendAppPath, parseAppUrl, relativeToAbsolute } from './utils';
import { appendAppPath, parseAppUrl, relativeToAbsolute, getAppInfo } from './utils';
interface SetupDeps {
context: ContextSetup;
@ -291,7 +291,10 @@ export class ApplicationService {
};
return {
applications$,
applications$: applications$.pipe(
map((apps) => new Map([...apps.entries()].map(([id, app]) => [id, getAppInfo(app)]))),
shareReplay(1)
),
capabilities,
currentAppId$: this.currentAppId$.pipe(
filter((appId) => appId !== undefined),

View file

@ -39,7 +39,9 @@ export {
AppLeaveAction,
AppLeaveDefaultAction,
AppLeaveConfirmAction,
LegacyApp,
PublicAppInfo,
PublicLegacyAppInfo,
// Internal types
InternalApplicationStart,
LegacyApp,
} from './types';

View file

@ -235,7 +235,7 @@ export interface App<HistoryLocationState = unknown> extends AppBase {
appRoute?: string;
}
/** @internal */
/** @public */
export interface LegacyApp extends AppBase {
appUrl: string;
subUrlBase?: string;
@ -243,6 +243,24 @@ export interface LegacyApp extends AppBase {
disableSubUrlTracking?: boolean;
}
/**
* Public information about a registered {@link App | application}
*
* @public
*/
export type PublicAppInfo = Omit<App, 'mount' | 'updater$'> & {
legacy: false;
};
/**
* Information about a registered {@link LegacyApp | legacy application}
*
* @public
*/
export type PublicLegacyAppInfo = Omit<LegacyApp, 'updater$'> & {
legacy: true;
};
/**
* A mount function called when the user navigates to this app's route.
*
@ -649,6 +667,15 @@ export interface ApplicationStart {
*/
capabilities: RecursiveReadonly<Capabilities>;
/**
* Observable emitting the list of currently registered apps and their associated status.
*
* @remarks
* Applications disabled by {@link Capabilities} will not be present in the map. Applications manually disabled from
* the client-side using an {@link AppUpdater | application updater} are present, with their status properly set as `inaccessible`.
*/
applications$: Observable<ReadonlyMap<string, PublicAppInfo | PublicLegacyAppInfo>>;
/**
* Navigate to a given app
*
@ -721,18 +748,7 @@ export interface ApplicationStart {
}
/** @internal */
export interface InternalApplicationStart
extends Pick<
ApplicationStart,
'capabilities' | 'navigateToApp' | 'navigateToUrl' | 'getUrlForApp' | 'currentAppId$'
> {
/**
* Apps available based on the current capabilities.
* Should be used to show navigation links and make routing decisions.
* Applications manually disabled from the client-side using {@link AppUpdater}
*/
applications$: Observable<ReadonlyMap<string, App | LegacyApp>>;
export interface InternalApplicationStart extends Omit<ApplicationStart, 'registerMountContext'> {
/**
* Register a context provider for application mounting. Will only be available to applications that depend on the
* plugin that registered this context. Deprecated, use {@link CoreSetup.getStartServices}.

View file

@ -17,7 +17,8 @@
* under the License.
*/
import { LegacyApp, App } from './types';
import { of } from 'rxjs';
import { LegacyApp, App, AppStatus, AppNavLinkStatus } from './types';
import { BasePath } from '../http/base_path';
import {
removeSlashes,
@ -25,6 +26,7 @@ import {
isLegacyApp,
relativeToAbsolute,
parseAppUrl,
getAppInfo,
} from './utils';
describe('removeSlashes', () => {
@ -459,3 +461,56 @@ describe('parseAppUrl', () => {
});
});
});
describe('getAppInfo', () => {
const createApp = (props: Partial<App> = {}): App => ({
mount: () => () => undefined,
updater$: of(() => undefined),
id: 'some-id',
title: 'some-title',
status: AppStatus.accessible,
navLinkStatus: AppNavLinkStatus.default,
appRoute: `/app/some-id`,
legacy: false,
...props,
});
const createLegacyApp = (props: Partial<LegacyApp> = {}): LegacyApp => ({
appUrl: '/my-app-url',
updater$: of(() => undefined),
id: 'some-id',
title: 'some-title',
status: AppStatus.accessible,
navLinkStatus: AppNavLinkStatus.default,
legacy: true,
...props,
});
it('converts an application and remove sensitive properties', () => {
const app = createApp();
const info = getAppInfo(app);
expect(info).toEqual({
id: 'some-id',
title: 'some-title',
status: AppStatus.accessible,
navLinkStatus: AppNavLinkStatus.default,
appRoute: `/app/some-id`,
legacy: false,
});
});
it('converts a legacy application and remove sensitive properties', () => {
const app = createLegacyApp();
const info = getAppInfo(app);
expect(info).toEqual({
appUrl: '/my-app-url',
id: 'some-id',
title: 'some-title',
status: AppStatus.accessible,
navLinkStatus: AppNavLinkStatus.default,
legacy: true,
});
});
});

View file

@ -18,7 +18,7 @@
*/
import { IBasePath } from '../http';
import { App, LegacyApp } from './types';
import { App, LegacyApp, PublicAppInfo, PublicLegacyAppInfo } from './types';
export interface AppUrlInfo {
app: string;
@ -119,3 +119,19 @@ const removeBasePath = (url: string, basePath: IBasePath, origin: string): strin
}
return basePath.remove(url);
};
export function getAppInfo(app: App<unknown> | LegacyApp): PublicAppInfo | PublicLegacyAppInfo {
if (isLegacyApp(app)) {
const { updater$, ...infos } = app;
return {
...infos,
legacy: true,
};
} else {
const { updater$, mount, ...infos } = app;
return {
...infos,
legacy: false,
};
}
}

View file

@ -21,7 +21,7 @@ import { shallow } from 'enzyme';
import React from 'react';
import * as Rx from 'rxjs';
import { take, toArray } from 'rxjs/operators';
import { App } from '../application';
import { App, PublicAppInfo } from '../application';
import { applicationServiceMock } from '../application/application_service.mock';
import { docLinksServiceMock } from '../doc_links/doc_links_service.mock';
import { httpServiceMock } from '../http/http_service.mock';
@ -29,6 +29,7 @@ import { injectedMetadataServiceMock } from '../injected_metadata/injected_metad
import { notificationServiceMock } from '../notifications/notifications_service.mock';
import { uiSettingsServiceMock } from '../ui_settings/ui_settings_service.mock';
import { ChromeService } from './chrome_service';
import { getAppInfo } from '../application/utils';
class FakeApp implements App {
public title = `${this.id} App`;
@ -55,8 +56,8 @@ function defaultStartDeps(availableApps?: App[]) {
};
if (availableApps) {
deps.application.applications$ = new Rx.BehaviorSubject<Map<string, App>>(
new Map(availableApps.map((app) => [app.id, app]))
deps.application.applications$ = new Rx.BehaviorSubject<Map<string, PublicAppInfo>>(
new Map(availableApps.map((app) => [app.id, getAppInfo(app) as PublicAppInfo]))
);
}

View file

@ -17,15 +17,12 @@
* under the License.
*/
import { App, AppMount, AppNavLinkStatus, AppStatus, LegacyApp } from '../../application';
import { PublicAppInfo, AppNavLinkStatus, AppStatus, PublicLegacyAppInfo } from '../../application';
import { toNavLink } from './to_nav_link';
import { httpServiceMock } from '../../mocks';
function mount() {}
const app = (props: Partial<App> = {}): App => ({
mount: (mount as unknown) as AppMount,
const app = (props: Partial<PublicAppInfo> = {}): PublicAppInfo => ({
id: 'some-id',
title: 'some-title',
status: AppStatus.accessible,
@ -35,7 +32,7 @@ const app = (props: Partial<App> = {}): App => ({
...props,
});
const legacyApp = (props: Partial<LegacyApp> = {}): LegacyApp => ({
const legacyApp = (props: Partial<PublicLegacyAppInfo> = {}): PublicLegacyAppInfo => ({
appUrl: '/my-app-url',
id: 'some-id',
title: 'some-title',

View file

@ -17,12 +17,15 @@
* under the License.
*/
import { App, AppNavLinkStatus, AppStatus, LegacyApp } from '../../application';
import { PublicAppInfo, AppNavLinkStatus, AppStatus, PublicLegacyAppInfo } from '../../application';
import { IBasePath } from '../../http';
import { NavLinkWrapper } from './nav_link';
import { appendAppPath } from '../../application/utils';
export function toNavLink(app: App | LegacyApp, basePath: IBasePath): NavLinkWrapper {
export function toNavLink(
app: PublicAppInfo | PublicLegacyAppInfo,
basePath: IBasePath
): NavLinkWrapper {
const useAppStatus = app.navLinkStatus === AppNavLinkStatus.default;
const relativeBaseUrl = isLegacyApp(app)
? basePath.prepend(app.appUrl)
@ -63,6 +66,6 @@ export function relativeToAbsolute(url: string) {
return a.href;
}
function isLegacyApp(app: App | LegacyApp): app is LegacyApp {
function isLegacyApp(app: PublicAppInfo | PublicLegacyAppInfo): app is PublicLegacyAppInfo {
return app.legacy === true;
}

View file

@ -104,6 +104,7 @@ export {
ApplicationSetup,
ApplicationStart,
App,
PublicAppInfo,
AppBase,
AppMount,
AppMountDeprecated,
@ -120,6 +121,8 @@ export {
AppUpdatableFields,
AppUpdater,
ScopedHistory,
LegacyApp,
PublicLegacyAppInfo,
} from './application';
export {

View file

@ -131,6 +131,7 @@ export class LegacyPlatformService {
const legacyCore: LegacyCoreStart = {
...core,
application: {
applications$: core.application.applications$,
currentAppId$: core.application.currentAppId$,
capabilities: core.application.capabilities,
getUrlForApp: core.application.getUrlForApp,

View file

@ -135,6 +135,7 @@ export function createPluginStartContext<
): CoreStart {
return {
application: {
applications$: deps.application.applications$,
currentAppId$: deps.application.currentAppId$,
capabilities: deps.application.capabilities,
navigateToApp: deps.application.navigateToApp,

View file

@ -106,6 +106,7 @@ export interface ApplicationSetup {
// @public (undocumented)
export interface ApplicationStart {
applications$: Observable<ReadonlyMap<string, PublicAppInfo | PublicLegacyAppInfo>>;
capabilities: RecursiveReadonly<Capabilities>;
currentAppId$: Observable<string | undefined>;
getUrlForApp(appId: string, options?: {
@ -857,6 +858,18 @@ export interface IUiSettingsClient {
set: (key: string, value: any) => Promise<boolean>;
}
// @public (undocumented)
export interface LegacyApp extends AppBase {
// (undocumented)
appUrl: string;
// (undocumented)
disableSubUrlTracking?: boolean;
// (undocumented)
linkToLastSubUrl?: boolean;
// (undocumented)
subUrlBase?: string;
}
// @public @deprecated
export interface LegacyCoreSetup extends CoreSetup<any, any> {
// Warning: (ae-forgotten-export) The symbol "InjectedMetadataSetup" needs to be exported by the entry point index.d.ts
@ -993,6 +1006,16 @@ export interface PluginInitializerContext<ConfigSchema extends object = object>
// @public (undocumented)
export type PluginOpaqueId = symbol;
// @public
export type PublicAppInfo = Omit<App, 'mount' | 'updater$'> & {
legacy: false;
};
// @public
export type PublicLegacyAppInfo = Omit<LegacyApp, 'updater$'> & {
legacy: true;
};
// @public
export type PublicUiSettingsParams = Omit<UiSettingsParams, 'schema'>;