Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extensions: Use annotations for extensions #2962

Merged
merged 37 commits into from
Oct 18, 2018
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
19fc919
Extensions: Use annotations for extensions. Add first set of extensio…
nwmac Sep 7, 2018
a2623d9
Fix some lint issues
nwmac Sep 7, 2018
1d54aaa
Merge branch 'v2-master' into ext-tabs-and-actions
nwmac Sep 7, 2018
3916c06
Fix max-line-length lint issue
nwmac Sep 9, 2018
db2d776
Fix a couple of code climate issues
nwmac Sep 9, 2018
84588b8
Fix unit tests. Correct example folder name
nwmac Sep 10, 2018
73f99f1
Tweaks to example theme
nwmac Sep 10, 2018
a8f87a7
Fix code climate issues
nwmac Sep 10, 2018
1d6d93a
Merge remote-tracking branch 'origin/v2-master' into ext-tabs-and-act…
nwmac Sep 11, 2018
6b147e7
Update package lock
nwmac Sep 11, 2018
c7506ea
Merge remote-tracking branch 'origin/v2-master' into ext-tabs-and-act…
nwmac Sep 11, 2018
baff023
Wire in remaining actions
nwmac Sep 11, 2018
120a8b4
Merge remote-tracking branch 'origin/v2-master' into ext-tabs-and-act…
nwmac Sep 21, 2018
d0fd16b
Remove example (will put in separate PR)
nwmac Sep 21, 2018
2dd5ae1
Merge remote-tracking branch 'origin/ext-tabs-and-actions' into ext-e…
nwmac Sep 21, 2018
9827d80
Add example
nwmac Sep 21, 2018
61ac011
Remove comma
nwmac Sep 21, 2018
f3f1887
Tidy ups
nwmac Sep 21, 2018
c1c451d
Merge branch 'ext-tabs-and-actions' of github.com:cloudfoundry-incuba…
nwmac Sep 21, 2018
2404465
Fix Extension init
nwmac Sep 21, 2018
c6a566e
Remove old depenency
nwmac Sep 27, 2018
82d3098
Merge remote-tracking branch 'origin/v2-master' into ext-tabs-and-act…
nwmac Oct 11, 2018
3a0325a
Remove whitespace to fix lint issue
nwmac Oct 11, 2018
96ee0a2
Remove white space
nwmac Oct 13, 2018
ef35f84
Fix merge issue
nwmac Oct 13, 2018
17b70fc
Fix for cancel not working for user management dialog
nwmac Oct 15, 2018
1c4ad30
Better fix for user mgmt stepper not being able to cancel
nwmac Oct 15, 2018
bad4dc4
Fix user mgmt nav issue
nwmac Oct 15, 2018
086626f
Move extension buttons component into the page header
KlapTrap Oct 16, 2018
d1f5de5
Tidy up and fixes
KlapTrap Oct 16, 2018
b60ce38
Merge branch 'ext-example' into ext-tabs-and-actions
KlapTrap Oct 16, 2018
b0e990c
Merge remote-tracking branch 'origin/ext-tabs-and-actions' into ext-e…
nwmac Oct 16, 2018
f6604ec
Compile fix and theme tweak
nwmac Oct 16, 2018
2d9c688
Merge branch 'ext-example' into ext-tabs-and-actions
KlapTrap Oct 16, 2018
9af9be2
Fix compilation error due to logoText not being added to Cuistomizato…
nwmac Oct 16, 2018
f9bff0a
Ensure extention buttons appear on the right, not next to the title
nwmac Oct 16, 2018
15023ef
Fix example
nwmac Oct 16, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5,222 changes: 2,601 additions & 2,621 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 3 additions & 4 deletions src/frontend/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ import { SharedModule } from './shared/shared.module';
import { AppStoreModule } from './store/store.module';
import { XSRFModule } from './xsrf.module';
import { GITHUB_API_URL, getGitHubAPIURL } from './core/github.helpers';
import { ExtensionService, applyRoutesFromExtensions } from './core/extension/extension-service';
import { Router } from '@angular/router';
import { ExtensionService } from './core/extension/extension-service';
import { DynamicExtenstionRoutes } from './core/extension/dynamic-extension-routes';

// Create action for router navigation. See
Expand Down Expand Up @@ -90,7 +89,7 @@ export class CustomRouterStateSerializer
bootstrap: [AppComponent]
})
export class AppModule {
constructor(private router: Router, private ext: ExtensionService) {
applyRoutesFromExtensions(router);
constructor(private ext: ExtensionService) {
ext.init();
}
}
31 changes: 23 additions & 8 deletions src/frontend/app/core/extension/dynamic-extension-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@ import { CanActivate, Router, RouterStateSnapshot, ActivatedRouteSnapshot, Route
import { Observable } from 'rxjs';
import { getRoutesFromExtensions, StratosRouteType } from './extension-service';

/**
* This is used to dynamically add an extension's routes - since we can't do this
* if the extentsion's module is lazy-loaded.
*
* This CanActive plugin typically is added to the route config to catch all unknown routes '**'
* When activated, it removes itself from the routing congif, so it only evern activates once.
*
* It checks if there are any new routes from extensions that need to be added and add them.
*
* Lastly, it navigates to the saem route that it intercepted - if a new extension route
* was added that now matches, it gets the route, otherwise the route goes up the chain
* as it would have before.
*/

@Injectable()
export class DynamicExtenstionRoutes implements CanActivate {
Expand All @@ -12,22 +25,24 @@ export class DynamicExtenstionRoutes implements CanActivate {
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean>|Promise<boolean>|boolean {
const childRoutes = this.getChildRoutes(route.parent.routeConfig);
// Remove the last route (which is us, the '**' route)
let newChildRoutes = childRoutes.splice(0, childRoutes.length - 1);

// Does the parent root have metadata to tell us what route group this is?
// i.e. are there extension routes we need to try and add?
if (route.routeConfig.data && route.routeConfig.data.stratosRouteGroup) {
const tabGroup = route.routeConfig.data.stratosRouteGroup;
const childRoutes = this.getChildRoutes(route.parent.routeConfig);

// Remove the last route
let newChildRoutes = childRoutes.splice(0, childRoutes.length - 1);

// Add the missing routes
const newRoutes = getRoutesFromExtensions(tabGroup as StratosRouteType);
newChildRoutes = newChildRoutes.concat(newRoutes);
this.setChildRoutes(route.parent.routeConfig, newChildRoutes);
this.router.navigateByUrl(state.url);
return false;
}
return true;

// Update the route config and navigate again to the same route that was intercepted
this.setChildRoutes(route.parent.routeConfig, newChildRoutes);
this.router.navigateByUrl(state.url);
return false;
}

private getChildRoutes(r: any) {
Expand Down
142 changes: 53 additions & 89 deletions src/frontend/app/core/extension/extension-service.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { Injectable, Component, Injector } from '@angular/core';
import { Injectable } from '@angular/core';
import { Route, Router } from '@angular/router';
import { AppModule } from '../../app.module';

export const extensionsActionRouteKey = 'extensionsActionsKey';

export interface EndpointTypeExtension {
type: string;
label: string;
authTypes: string[];
}

export interface RouteInfo {
target: any;
name: string;
export interface StratosExtensionConfig {
routes?: Route[];
}

// The different types of Tab
export enum StratosTabType {
Application = 'appTabs',
CloudFoundry = 'cfTabs',
Expand All @@ -26,6 +27,7 @@ export interface StratosTabMetadata {
link: string;
}

// The different types of Action
export enum StratosActionType {
Applications = 'appsActions',
Application = 'appActions',
Expand All @@ -45,6 +47,7 @@ export interface StratosActionMetadata {

export type StratosRouteType = StratosTabType | StratosActionType;

// Stores the extension metadata as defined by the decorators
const extensionMetadata = {
routes: [],
loginComponent: null,
Expand All @@ -53,6 +56,42 @@ const extensionMetadata = {
actions: {},
};

/**
* Decortator for a Tab extension
*/
export function StratosTab(props: StratosTabMetadata) {
return function (target) {
addExtensionTab(props.type, target, props);
};
}

/**
* Decortator for an Action extension
*/
export function StratosAction(props: StratosActionMetadata) {
return function (target) {
addExtensionAction(props.type, target, props);
};
}

/**
* Decorator for an Extension module providing routes etc.
*/

export function StratosExtension(config: StratosExtensionConfig) {
return (_target) => {
if (config.routes) {
extensionMetadata.routes.push(config.routes);
}
};
}

export function StratosLoginComponent() {
return (target) => {
extensionMetadata.loginComponent = target;
};
}

function addExtensionTab(tab: StratosTabType, target: any, props: any) {
if (!extensionMetadata.tabs[tab]) {
extensionMetadata.tabs[tab] = [];
Expand Down Expand Up @@ -80,103 +119,25 @@ function addExtensionAction(action: StratosActionType, target: any, props: any)
extensionMetadata.actions[action].push(props);
}

export function StratosTab(props: StratosTabMetadata) {
return function(target) {
addExtensionTab(props.type, target, props);
};
}

export function StratosAction(props: StratosActionMetadata) {
return function(target) {
addExtensionAction(props.type, target, props);
};
}


export interface StratosNavExtensionConfig {
routes: Route[];
}

export function StratosNavExtension(config: StratosNavExtensionConfig) {
return (target) => {
extensionMetadata.routes.push(config.routes);
};
}

export function StratosLoginComponent() {
return (target) => {
extensionMetadata.loginComponent = target;
};
}


// Injectable Extension Service
@Injectable()
export class ExtensionService {

private routes: Route[] = [];

private endointTypes: EndpointTypeExtension[] = [];

private loginComponent: any;

public metadata = extensionMetadata;

constructor(private router: Router, private injector: Injector) {}

/**
* Regiser new top-level routes
*/
public registerRoutes(r: Route[]): ExtensionService {
this.routes = this.routes.concat(r);
return this;
}
constructor(private router: Router) { }

/**
* Register a component to use for the Login page
* Initialize the extensions - to be invoked in the AppModule
*/
public registerLoginComponent(component: any): ExtensionService {
this.loginComponent = component;
return this;
}

public registerEndpointType(epType: EndpointTypeExtension): ExtensionService {
this.endointTypes.push(epType);
return this;
}

public getEndpointTypes(): EndpointTypeExtension[] {
return this.endointTypes;
public init() {
this.applyRoutesFromExtensions(this.router);
}

/**
* Apply route configuration
*/
public applyRouteConfig() {
const routeConfig = [...this.router.config];
const dashboardRoute = routeConfig.find(r => r.path === '' && !!r.component && r.component.name === 'DashboardBaseComponent');
let needsReset = false;
if (dashboardRoute) {
dashboardRoute.children = [
...dashboardRoute.children,
...this.routes
];
needsReset = true;
}

if (this.loginComponent) {
// Override the component used for the login route
const loginRoute = routeConfig.find(r => r.path === 'login') || {};
loginRoute.component = this.loginComponent;
needsReset = true;
}

if (needsReset) {
this.router.resetConfig(routeConfig);
}
}
}

export function applyRoutesFromExtensions(router: Router) {
private applyRoutesFromExtensions(router: Router) {
const routeConfig = [...router.config];
const dashboardRoute = routeConfig.find(r => r.path === '' && !!r.component && r.component.name === 'DashboardBaseComponent');
let needsReset = false;
Expand All @@ -195,8 +156,11 @@ export function applyRoutesFromExtensions(router: Router) {
if (needsReset) {
router.resetConfig(routeConfig);
}
}
}

// Helpers to access Extension metadata (without using the injectable Extension Service)

export function getRoutesFromExtensions(routeType: StratosRouteType) {
return extensionMetadata.extensionRoutes[routeType] || [];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe('ApplicationDeleteComponent', () => {
TestBed.configureTestingModule({
imports: [
...BaseTestModules,
ApplicationsModule,
ApplicationsModule
],
providers: [
generateTestEntityServiceProvider(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ <h1>Applications</h1>
<button id="appwall-deploy-application" mat-icon-button [routerLink]="'/applications/deploy/'">
<mat-icon>file_upload</mat-icon>
</button>
<!-- Extension buttons -->
<app-extension-buttons type="Applications"></app-extension-buttons>
</ng-container>
</ng-container>
</div>
Expand All @@ -30,4 +28,4 @@ <h1>Applications</h1>
}"></app-no-content-message>
</ng-template>
<app-list [noEntries]="noEntries" [noEntriesForCurrentFilter]="noEntriesForCurrentFilter" *ngIf="!!(cloudFoundryService.hasRegisteredCFEndpoints$ | async) && !!(cloudFoundryService.hasConnectedCFEndpoints$ | async)">
</app-list>
</app-list>
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,47 @@
<div *ngIf="applicationService.application$ | async as application" class="app-page-header">
<h1>{{ application?.app?.entity?.name }} </h1>
<div>
<a class="app-page-header__anchor" mat-icon-button *ngIf="(applicationService.applicationUrl$ | async) != null && application?.app.entity.state === 'STARTED'" href="{{applicationService.applicationUrl$ | async}}" target="_blank" matTooltip="Visit">
<a class="app-page-header__anchor" mat-icon-button *ngIf="(applicationService.applicationUrl$ | async) != null && application?.app.entity.state === 'STARTED'"
href="{{applicationService.applicationUrl$ | async}}" target="_blank" matTooltip="Visit">
<mat-icon>launch</mat-icon>
</a>
<span *ngIf="isBusyUpdating$ | async as busy">
<span *ngIf="applicationService.appSpace$ | async as space">
<span class="manage-application-actions" *appUserPermission="manageAppPermission;spaceGuid:space.metadata.guid;endpointGuid:this.applicationService.cfGuid">
<button mat-icon-button name="edit" *ngIf="(this.applicationService.applicationState$ | async)" routerLink="/applications/{{applicationService.cfGuid}}/{{applicationService.appGuid}}/edit" matTooltip="Edit">
<mat-icon>edit</mat-icon>
<button mat-icon-button name="edit" *ngIf="(this.applicationService.applicationState$ | async)" routerLink="/applications/{{applicationService.cfGuid}}/{{applicationService.appGuid}}/edit"
matTooltip="Edit">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button name="delete" (click)="redirectToDeletePage()" matTooltip="Delete">
<mat-icon>delete</mat-icon>
</button>
<div *ngIf="applicationService.applicationState$ | async as appState" class="app-page-header__actions">
<button mat-icon-button name="restart" [disabled]="busy.updating || !appState.actions.restart" (click)="restartApplication()" matTooltip="Restart">
<mat-icon>settings_backup_restore</mat-icon>
</button>
<button mat-icon-button name="stop" [disabled]="busy.updating" *ngIf="appState.actions.stop;else startButton" (click)="stopApplication()" matTooltip="Stop">
<mat-icon>stop</mat-icon>
</button>
<ng-template #startButton>
<button mat-icon-button name="start" [disabled]="busy.updating || !appState.actions.start && !appState.actions.stop" (click)="startApplication()" matTooltip="Start">
<mat-icon>play_arrow</mat-icon>
<button mat-icon-button name="restart" [disabled]="busy.updating || !appState.actions.restart" (click)="restartApplication()"
matTooltip="Restart">
<mat-icon>settings_backup_restore</mat-icon>
</button>
<button mat-icon-button name="stop" [disabled]="busy.updating" *ngIf="appState.actions.stop;else startButton"
(click)="stopApplication()" matTooltip="Stop">
<mat-icon>stop</mat-icon>
</button>
<ng-template #startButton>
<button mat-icon-button name="start" [disabled]="busy.updating || !appState.actions.start && !appState.actions.stop"
(click)="startApplication()" matTooltip="Start">
<mat-icon>play_arrow</mat-icon>
</button>
</ng-template>
<button mat-icon-button name="restage" [disabled]="busy.updating || !appState.actions.restage" (click)="restageApplication()"
matTooltip="Restage">
<mat-icon>redo</mat-icon>
</button>
</ng-template>
<button mat-icon-button name="restage" [disabled]="busy.updating || !appState.actions.restage" (click)="restageApplication()" matTooltip="Restage">
<mat-icon>redo</mat-icon>
</button>
</div>
</span>
</span>
<!-- Extension buttons -->
<app-extension-buttons type="Application"></app-extension-buttons>
</span>
</div>
</div>
</app-page-header>
<app-loading-page [entityId]="applicationService.appGuid" [entitySchema]="schema" deleteText="Deleting application" text="Retrieving application" class="router-component">
<app-loading-page [entityId]="applicationService.appGuid" [entitySchema]="schema" deleteText="Deleting application"
text="Retrieving application" class="router-component">
<router-outlet></router-outlet>
</app-loading-page>
</app-loading-page>
Loading