Skip to content

Commit

Permalink
[wip] pull navlinks from app service
Browse files Browse the repository at this point in the history
  • Loading branch information
joshdover committed Apr 16, 2019
1 parent cf1d0ed commit 1c5a67a
Show file tree
Hide file tree
Showing 17 changed files with 262 additions and 86 deletions.
76 changes: 75 additions & 1 deletion src/core/public/application/application_service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,92 @@
* under the License.
*/

import { Observable, BehaviorSubject, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import { CapabilitiesSetup, CapabilitiesService } from './capabilities';
import { InjectedMetadataSetup } from '../injected_metadata';

interface BaseApp {
id: string;

/**
* An ordinal used to sort nav links relative to one another for display.
*/
order: number;

/**
* The title of the application.
*/
title: string;

/**
* An observable for a tooltip shown when hovering over app link.
*/
tooltip$?: Observable<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;
}

export interface App extends BaseApp {
rootRoute: string;

mount(targetDomElement: HTMLElement): () => void;
}

export interface LegacyApp extends BaseApp {
appUrl: string;

url?: string;
}

export interface ApplicationServiceSetup {
availableApps$: Observable<Array<App | LegacyApp>>;
capabilities$: CapabilitiesSetup['capabilities$'];
mount: (mountHandler: Function) => void;
registerApp(app: App): void;
registerLegacyApp(app: LegacyApp): void;
}

interface SetupDeps {
injectedMetadata: InjectedMetadataSetup;
}

/**
* Service that is responsible for registering new applications.
* @internal
*/
export class ApplicationService {
public setup(): ApplicationServiceSetup {
private readonly capabilities = new CapabilitiesService();

public setup({ injectedMetadata }: SetupDeps): ApplicationServiceSetup {
const apps$ = new BehaviorSubject<App[]>([]);
const legacyApps$ = new BehaviorSubject<LegacyApp[]>([]);
const allApps$ = combineLatest(apps$, legacyApps$).pipe(
map(([apps, legacyApps]) => [...apps, ...legacyApps])
);

const capabiltiesSetup = this.capabilities.setup({ injectedMetadata, apps$: allApps$ });

return {
capabilities$: capabiltiesSetup.capabilities$,
availableApps$: capabiltiesSetup.availableApps$,
mount(mountHandler: Function) {},
registerApp(app: App) {
apps$.next([...apps$.value, app]);
},
registerLegacyApp(app: LegacyApp) {
legacyApps$.next([...legacyApps$.value, app]);
},
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { InjectedMetadataService } from '../injected_metadata';
import { InjectedMetadataService } from '../../injected_metadata';
import { CapabilitiesService } from './capabilities_service';

describe('#start', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,16 @@
* specific language governing permissions and limitations
* under the License.
*/
import { InjectedMetadataSetup } from '../injected_metadata';
import { deepFreeze } from '../utils/deep_freeze';

interface StartDeps {
import { debounceTime, concatMap } from 'rxjs/operators';
import { from, Observable, BehaviorSubject } from 'rxjs';

import { InjectedMetadataSetup } from '../../injected_metadata';
import { deepFreeze, RecursiveReadonly } from '../../utils/deep_freeze';
import { App, LegacyApp } from '../application_service';

interface SetupDeps {
apps$: Observable<Array<App | LegacyApp>>;
injectedMetadata: InjectedMetadataSetup;
}

Expand Down Expand Up @@ -54,7 +60,12 @@ export interface CapabilitiesSetup {
/**
* Gets the read-only capabilities.
*/
getCapabilities: () => Capabilities;
capabilities$: Observable<RecursiveReadonly<Capabilities>>;

/**
* Apps available based on the current capabilities.
*/
availableApps$: Observable<Array<App | LegacyApp>>;
}

/** @internal */
Expand All @@ -63,10 +74,34 @@ export interface CapabilitiesSetup {
* Service that is responsible for UI Capabilities.
*/
export class CapabilitiesService {
public setup({ injectedMetadata }: StartDeps): CapabilitiesSetup {
public setup({ injectedMetadata, apps$ }: SetupDeps): CapabilitiesSetup {
const capabilities$ = new BehaviorSubject<Capabilities>(
deepFreeze(injectedMetadata.getInjectedVar('uiCapabilities') as Capabilities)
);

const availableApps$ = apps$.pipe(
// Avoid slamming the server.
// NOTE: when `start` lifeycle is introduced, only run this once,
// when all apps have already been registered.
debounceTime(500),
concatMap(apps =>
from(
(async function() {
// TODO: need this endpoint to actually exist
// const res = await fetch('/api/security/capabilities', {
// body: JSON.stringify({ apps }),
// });
// capabilities$.next(deepFreeze((await res.json()) as Capabilities));
// TODO: filter `apps` based on capabilties response
return apps;
})()
)
)
);

return {
getCapabilities: () =>
deepFreeze<Capabilities>(injectedMetadata.getInjectedVar('uiCapabilities') as Capabilities),
capabilities$: capabilities$.asObservable(),
availableApps$,
};
}
}
File renamed without changes.
7 changes: 5 additions & 2 deletions src/core/public/chrome/chrome_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { BasePathSetup } from '../base_path';
import { InjectedMetadataSetup } from '../injected_metadata';
import { NotificationsSetup } from '../notifications';
import { NavLinksService } from './nav_links/nav_links_service';
import { ApplicationServiceSetup } from '../application';

const IS_COLLAPSED_KEY = 'core.chrome.isCollapsed';

Expand Down Expand Up @@ -55,6 +56,7 @@ interface ConstructorParams {
}

interface SetupDeps {
application: ApplicationServiceSetup;
basePath: BasePathSetup;
injectedMetadata: InjectedMetadataSetup;
notifications: NotificationsSetup;
Expand All @@ -70,7 +72,7 @@ export class ChromeService {
this.browserSupportsCsp = browserSupportsCsp;
}

public setup({ basePath, injectedMetadata, notifications }: SetupDeps) {
public setup({ application, basePath, injectedMetadata, notifications }: SetupDeps) {
const FORCE_HIDDEN = isEmbedParamInHash();

const brand$ = new Rx.BehaviorSubject<ChromeBrand>({});
Expand Down Expand Up @@ -186,7 +188,8 @@ export class ChromeService {
setHelpExtension: (helpExtension?: ChromeHelpExtension) => {
helpExtension$.next(helpExtension);
},
navLinks: this.navLinks.setup(basePath),

navLinks: this.navLinks.setup(application, basePath),
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/core/public/chrome/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ export {
ChromeBrand,
ChromeHelpExtension,
} from './chrome_service';
export { ChromeNavLinkProperties } from './nav_links';
export { ChromeNavLink } from './nav_links';
2 changes: 1 addition & 1 deletion src/core/public/chrome/nav_links/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@
* under the License.
*/

export { ChromeNavLinkProperties } from './nav_link';
export { ChromeNavLink } from './nav_link';
export { NavLinksService } from './nav_links_service';
14 changes: 7 additions & 7 deletions src/core/public/chrome/nav_links/nav_link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { BasePathSetup } from '../../base_path';
/**
* @public
*/
export interface ChromeNavLinkProperties {
export interface ChromeNavLink {
/**
* A unique identifier for looking up links.
*/
Expand Down Expand Up @@ -64,7 +64,7 @@ export interface ChromeNavLinkProperties {
/**
* A tooltip shown when hovering over an app link.
*/
tooltip: string;
tooltip?: string;

/**
* The base URL used to open the root of an application.
Expand Down Expand Up @@ -94,14 +94,14 @@ export interface ChromeNavLinkProperties {
}

export type NavLinkUpdateableFields = Partial<
Pick<ChromeNavLinkProperties, 'active' | 'disabled' | 'hidden' | 'url'>
Pick<ChromeNavLink, 'active' | 'disabled' | 'hidden' | 'url'>
>;

export class NavLink {
export class NavLinkWrapper {
public readonly id: string;
public readonly properties: Readonly<ChromeNavLinkProperties>;
public readonly properties: Readonly<ChromeNavLink>;

constructor(properties: ChromeNavLinkProperties, private readonly basePath: BasePathSetup) {
constructor(properties: ChromeNavLink, private readonly basePath: BasePathSetup) {
if (!properties || !properties.id) {
throw new Error('`id` is required.');
}
Expand All @@ -111,6 +111,6 @@ export class NavLink {
}

public update(newProps: NavLinkUpdateableFields) {
return new NavLink({ ...this.properties, ...newProps }, this.basePath);
return new NavLinkWrapper({ ...this.properties, ...newProps }, this.basePath);
}
}
55 changes: 46 additions & 9 deletions src/core/public/chrome/nav_links/nav_links_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,64 @@
*/

import { sortBy } from 'lodash';
import { BehaviorSubject, ReplaySubject } from 'rxjs';
import { map, takeUntil } from 'rxjs/operators';
import {
BehaviorSubject,
ReplaySubject,
Observable,
combineLatest,
from,
isObservable,
} from 'rxjs';
import { map, takeUntil, concatMap } from 'rxjs/operators';
import { BasePathSetup } from '../../base_path';
import { ChromeNavLinkProperties, NavLink, NavLinkUpdateableFields } from './nav_link';
import { ChromeNavLink, NavLinkWrapper, NavLinkUpdateableFields } from './nav_link';
import { ApplicationServiceSetup } from '../../application';
import { LegacyApp, App } from '../../application/application_service';

export type NavLinkFilter = (link: ChromeNavLink) => boolean;

export class NavLinksService {
private readonly stop$ = new ReplaySubject(1);

public setup(basePath: BasePathSetup) {
const navLinks$ = new BehaviorSubject<ReadonlyArray<NavLink>>([]);
public setup(application: ApplicationServiceSetup, basePath: BasePathSetup) {
const navLinks$ = new BehaviorSubject<ReadonlyArray<NavLinkWrapper>>([]);

// Generate app nav links for all legacy apps
// TODO: add for non-legacy apps
const appNavLinks$: Observable<NavLinkWrapper[]> = application.availableApps$.pipe(
map(apps =>
apps.map(
app =>
new NavLinkWrapper(
{
...app,
active: false,
disabled: false,
hidden: false,
appUrl: (app as LegacyApp).appUrl || (app as App).rootRoute,
},
basePath
)
)
)
);

// Combine nav links from ApplicationService and manual nav links
// TODO: remove once manual nav links are gone.
const allNavLinks$: Observable<NavLinkWrapper[]> = combineLatest(navLinks$, appNavLinks$).pipe(
map(([navLinks, appNavLinks]) => [...navLinks, ...appNavLinks])
);

return {
getNavLinks$: () => {
return navLinks$.pipe(
return allNavLinks$.pipe(
map(sortNavLinks),
takeUntil(this.stop$)
);
},

add(navLink: ChromeNavLinkProperties) {
navLinks$.next([...navLinks$.value, new NavLink(navLink, basePath)]);
add(navLink: ChromeNavLink) {
navLinks$.next([...navLinks$.value, new NavLinkWrapper(navLink, basePath)]);
},

clear() {
Expand Down Expand Up @@ -83,6 +120,6 @@ export class NavLinksService {
}
}

function sortNavLinks(navLinks: ReadonlyArray<NavLink>) {
function sortNavLinks(navLinks: ReadonlyArray<NavLinkWrapper>) {
return sortBy(navLinks.map(link => link.properties), 'order');
}
Loading

0 comments on commit 1c5a67a

Please sign in to comment.