src/frontend/packages/core/src/features/dashboard/dashboard-base/dashboard-base.component.ts
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { Portal } from '@angular/cdk/portal';
import { AfterViewInit, Component, NgZone, OnDestroy, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
import { MatDrawer } from '@angular/material/sidenav';
import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Route, Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { combineLatest, Observable, of, Subscription } from 'rxjs';
import { distinctUntilChanged, filter, map, startWith, withLatestFrom } from 'rxjs/operators';
import { CloseSideNav, DisableMobileNav, EnableMobileNav } from '../../../../../store/src/actions/dashboard-actions';
import { GetCurrentUsersRelations } from '../../../../../store/src/actions/permissions.actions';
import { DashboardOnlyAppState } from '../../../../../store/src/app-state';
import { entityCatalog } from '../../../../../store/src/entity-catalog/entity-catalog';
import { selectDashboardState } from '../../../../../store/src/selectors/dashboard.selectors';
import { stratosEntityCatalog } from '../../../../../store/src/stratos-entity-catalog';
import { DashboardState } from '../../../../../store/src/types/dashboard.types';
import { CustomizationService } from '../../../core/customizations.types';
import { EndpointsService } from '../../../core/endpoints.service';
import { IHeaderBreadcrumbLink } from '../../../shared/components/page-header/page-header.types';
import { SidePanelMode, SidePanelService } from '../../../shared/services/side-panel.service';
import { TabNavService } from '../../../tab-nav.service';
import { IPageSideNavTab } from '../page-side-nav/page-side-nav.component';
import { PageHeaderService } from './../../../core/page-header-service/page-header.service';
import { SideNavItem } from './../side-nav/side-nav.component';
@Component({
selector: 'app-dashboard-base',
templateUrl: './dashboard-base.component.html',
styleUrls: ['./dashboard-base.component.scss']
})
export class DashboardBaseComponent implements OnInit, OnDestroy, AfterViewInit {
public activeTabLabel$: Observable<string>;
public subNavData$: Observable<[string, Portal<any>, IPageSideNavTab, IHeaderBreadcrumbLink[]]>;
public isMobile$: Observable<boolean>;
public sideNavMode$: Observable<string>;
public sideNavMode: string;
public mainNavState$: Observable<{ mode: string; opened: boolean; iconMode: boolean, }>;
public rightNavState$: Observable<{ opened: boolean, component?: object, props?: object, }>;
private dashboardState$: Observable<DashboardState>;
public noMargin$: Observable<boolean>;
private closeSub: Subscription;
private mobileSub: Subscription;
private drawer: MatDrawer;
public iconModeOpen = false;
public sideNavWidth = 54;
sideNavTabs: SideNavItem[] = this.getNavigationRoutes();
sideNaveMode = 'side';
@ViewChild('previewPanelContainer', { read: ViewContainerRef }) previewPanelContainer: ViewContainerRef;
@ViewChild('content') public content;
// Slide-in side panel mode
sidePanelMode: SidePanelMode = SidePanelMode.Modal;
constructor(
public pageHeaderService: PageHeaderService,
private store: Store<DashboardOnlyAppState>,
private breakpointObserver: BreakpointObserver,
private router: Router,
private activatedRoute: ActivatedRoute,
private endpointsService: EndpointsService,
public tabNavService: TabNavService,
private ngZone: NgZone,
public sidePanelService: SidePanelService,
private cs: CustomizationService
) {
this.noMargin$ = this.router.events.pipe(
filter(event => event instanceof NavigationEnd),
map(() => this.isNoMarginView(this.activatedRoute.snapshot)),
startWith(this.isNoMarginView(this.activatedRoute.snapshot))
);
this.isMobile$ = this.breakpointObserver.observe([Breakpoints.Small, Breakpoints.XSmall]).pipe(
map(breakpoint => breakpoint.matches),
startWith(false),
distinctUntilChanged()
);
this.dashboardState$ = this.store.select(selectDashboardState);
this.mainNavState$ = this.dashboardState$.pipe(
map(state => {
if (state.isMobile) {
return {
mode: 'over',
opened: state.isMobileNavOpen || false,
iconMode: false
};
} else {
return {
mode: state.sideNavPinned ? 'side' : 'over',
opened: true,
iconMode: !state.sidenavOpen
};
}
})
);
this.mobileSub = this.isMobile$
.subscribe(isMobile => isMobile ? this.store.dispatch(new EnableMobileNav()) : this.store.dispatch(new DisableMobileNav()));
}
@ViewChild('sidenav') set sidenav(drawer: MatDrawer) {
this.drawer = drawer;
if (!this.closeSub) {
// We need this for mobile to ensure the state is synced when the dashboard is closed by clicking on the backdrop.
this.closeSub = drawer.closedStart.pipe(withLatestFrom(this.dashboardState$)).subscribe(([change, state]) => {
if (state.isMobile) {
this.store.dispatch(new CloseSideNav());
}
});
}
}
public redrawSideNav() {
// We need to do this to ensure there isn't a space left behind
// when going from mobile to desktop
this.ngZone.runOutsideAngular(() => {
setTimeout(() => this.drawer._modeChanged.next(), 250);
});
}
dispatchRelations() {
this.store.dispatch(new GetCurrentUsersRelations());
}
sideHelpClosed() {
this.sidePanelService.hide();
}
ngAfterViewInit() {
this.sidePanelService.setContainer(this.previewPanelContainer);
}
ngOnInit() {
this.subNavData$ = combineLatest(
this.tabNavService.getCurrentTabHeaderObservable().pipe(
startWith(null)
),
this.tabNavService.tabSubNav$,
this.tabNavService.tabSubNavBreadcrumbs$
).pipe(map(([tabNav, tabSubNav, tabSubNavBreadcrumb]) => [tabNav ? tabNav.label : null, tabSubNav, tabNav, tabSubNavBreadcrumb]));
// Register all health checks for endpoint types that support this
entityCatalog.getAllEndpointTypes().forEach(epType => {
if (epType && epType.definition && epType.definition.healthCheck) {
this.endpointsService.registerHealthCheck(epType.definition.healthCheck);
}
});
this.dispatchRelations();
stratosEntityCatalog.userFavorite.api.getAll();
}
ngOnDestroy() {
this.mobileSub.unsubscribe();
this.closeSub.unsubscribe();
this.sidePanelService.unsetContainer();
}
isNoMarginView(route: ActivatedRouteSnapshot): boolean {
while (route.firstChild) {
route = route.firstChild;
if (route.data.uiNoMargin) {
return true;
}
}
return false;
}
private getNavigationRoutes(): SideNavItem[] {
let navItems = this.collectNavigationRoutes('', this.router.config);
// Sort by name
navItems = navItems.sort((a: SideNavItem, b: SideNavItem) => a.label.localeCompare(b.label));
// Sort by position
navItems = navItems.sort((a: SideNavItem, b: SideNavItem) => {
const posA = a.position ? a.position : 99;
const posB = b.position ? b.position : 99;
return posA - posB;
});
return navItems;
}
private collectNavigationRoutes(path: string, routes: Route[]): SideNavItem[] {
if (!routes) {
return [];
}
return routes.reduce((nav, route) => {
if (route.data && route.data.stratosNavigation) {
const item: SideNavItem = {
...route.data.stratosNavigation,
link: path + '/' + route.path
};
if (item.requiresEndpointType) {
// Upstream always likes to show Cloud Foundry related endpoints - other distributions can change this behaviour
const alwaysShow = this.cs.get().alwaysShowNavForEndpointTypes ?
this.cs.get().alwaysShowNavForEndpointTypes(item.requiresEndpointType) : (item.requiresEndpointType === 'cf');
item.hidden = alwaysShow ? of(false) : this.endpointsService.doesNotHaveConnectedEndpointType(item.requiresEndpointType);
} else if (item.requiresPersistence) {
item.hidden = this.endpointsService.disablePersistenceFeatures$.pipe(startWith(true));
}
// Backwards compatibility (text became label)
if (!item.label && !!item.text) {
item.label = item.text;
}
nav.push(item);
}
const navs = this.collectNavigationRoutes(route.path, route.children);
return nav.concat(navs);
}, []);
}
}