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 3f83a08
Show file tree
Hide file tree
Showing 12 changed files with 283 additions and 71 deletions.
66 changes: 66 additions & 0 deletions src/core/public/application/application_service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,59 @@
* under the License.
*/

import { Observable, BehaviorSubject, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';

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 {
mount: (mountHandler: Function) => void;
registerApp(app: App): void;
registerLegacyApp(app: LegacyApp): void;
allApps$: Observable<BaseApp[]>;
apps$: Observable<App[]>;
legacyApps$: Observable<LegacyApp[]>;
}

/**
Expand All @@ -27,8 +78,23 @@ export interface ApplicationServiceSetup {
*/
export class ApplicationService {
public setup(): ApplicationServiceSetup {
const apps$ = new BehaviorSubject<App[]>([]);
const legacyApps$ = new BehaviorSubject<LegacyApp[]>([]);
const allApps$ = combineLatest(apps$, legacyApps$).pipe(
map(([apps, legacyApps]) => [...apps, ...legacyApps])
);

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

Expand Down
45 changes: 43 additions & 2 deletions src/core/public/capabilities/capabilities_service.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,18 @@
* specific language governing permissions and limitations
* under the License.
*/

import { debounceTime, map, concatMap } from 'rxjs/operators';
import { Subject, from } from 'rxjs';

import { InjectedMetadataSetup } from '../injected_metadata';
import { deepFreeze } from '../utils/deep_freeze';
import { ApplicationServiceSetup } from '../application';
import { ChromeSetup, ChromeNavLink } from '../chrome';

interface StartDeps {
interface SetupDeps {
application: ApplicationServiceSetup;
chrome: ChromeSetup;
injectedMetadata: InjectedMetadataSetup;
}

Expand Down Expand Up @@ -57,13 +65,46 @@ export interface CapabilitiesSetup {
getCapabilities: () => Capabilities;
}

function hasCapabilitiesForLink(caps: any, link: ChromeNavLink) {
// TODO: some logic to read the capabilities and see if link should be shown.
return true;
}

/** @internal */

/**
* Service that is responsible for UI Capabilities.
*/
export class CapabilitiesService {
public setup({ injectedMetadata }: StartDeps): CapabilitiesSetup {
public setup({ application, chrome, injectedMetadata }: SetupDeps): CapabilitiesSetup {
// TODO: `any` should be the shape of the capabilities API response
const capabilities$ = new Subject<any>();

application.allApps$
.pipe(
debounceTime(500),
// TODO: need this endpoint to actually exist
concatMap(apps =>
from(
(async function() {
const res = await fetch('/api/security/capabilities', {
body: JSON.stringify({ apps }),
});
// do something with response
return true;
})()
)
)
)
.subscribe(capabilities => {
capabilities$.next(capabilities);
});

// Register an Observable link filter for showing chrome nav links
chrome.navLinks.addLinkFilter(
capabilities$.pipe(map(caps => link => hasCapabilitiesForLink(caps, link)))
);

return {
getCapabilities: () =>
deepFreeze<Capabilities>(injectedMetadata.getInjectedVar('uiCapabilities') as Capabilities),
Expand Down
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);
}
}
86 changes: 77 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,86 @@
*/

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';

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>>([]);
const linkFilters$ = new BehaviorSubject<ReadonlyArray<Observable<NavLinkFilter>>>([
from([() => true]),
]);

// Generate app nav links for all legacy apps
// TODO: add for non-legacy apps
const appNavLinks$: Observable<NavLinkWrapper[]> = application.legacyApps$.pipe(
map(apps =>
apps.map(
app =>
new NavLinkWrapper(
{
...app,
active: false,
disabled: false,
hidden: false,
},
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])
);

// Unwrap the filter observables
const latestFilters$: Observable<NavLinkFilter[]> = linkFilters$.pipe(
concatMap(filters => combineLatest(...filters))
);
// Filter nav links. Each link must pass every filter to be included.
const filteredNavLinks$: Observable<NavLinkWrapper[]> = combineLatest(
allNavLinks$,
latestFilters$
).pipe(
map(([navLinks, filters]) =>
navLinks.filter(link =>
filters.reduce(
(passed, filter) => {
return passed && filter(link.properties);
},
true as boolean
)
)
)
);

return {
getNavLinks$: () => {
return navLinks$.pipe(
return filteredNavLinks$.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 All @@ -62,6 +121,15 @@ export class NavLinksService {
navLinks$.next(navLinks$.value.filter(link => link.id === id));
},

/**
* Add a filter to the links displayed in the global navigation.
* @param filter A filter function or an Observable that emits filter functions.
*/
addLinkFilter(filter: NavLinkFilter | Observable<NavLinkFilter>) {
const filter$ = isObservable(filter) ? filter : from([filter]);
linkFilters$.next([...linkFilters$.value, filter$]);
},

update(id: string, values: NavLinkUpdateableFields) {
if (!this.exists(id)) {
return;
Expand All @@ -83,6 +151,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 3f83a08

Please sign in to comment.