import { NgModule } from '@angular/core';
-import { RouterModule, Routes } from '@angular/router';
+import { ActivatedRouteSnapshot, RouterModule, Routes } from '@angular/router';
import { IscsiComponent } from './ceph/block/iscsi/iscsi.component';
import { MirroringComponent } from './ceph/block/mirroring/mirroring.component';
import { UserListComponent } from './core/auth/user-list/user-list.component';
import { ForbiddenComponent } from './core/forbidden/forbidden.component';
import { NotFoundComponent } from './core/not-found/not-found.component';
+import { BreadcrumbsResolver, IBreadcrumb } from './shared/models/breadcrumbs';
import { AuthGuardService } from './shared/services/auth-guard.service';
import { ModuleStatusGuardService } from './shared/services/module-status-guard.service';
+export class PerformanceCounterBreadcrumbsResolver extends BreadcrumbsResolver {
+ resolve(route: ActivatedRouteSnapshot) {
+ const result: IBreadcrumb[] = [];
+
+ const fromPath = route.queryParams.fromLink || null;
+ let fromText = '';
+ switch (fromPath) {
+ case '/monitor':
+ fromText = 'Monitors';
+ break;
+ case '/hosts':
+ fromText = 'Hosts';
+ break;
+ }
+ result.push({ text: 'Cluster', path: null });
+ result.push({ text: fromText, path: fromPath });
+ result.push({ text: 'Performance Counters', path: '' });
+
+ return result;
+ }
+}
+
const routes: Routes = [
+ // Dashboard
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuardService] },
- { path: 'hosts', component: HostsComponent, canActivate: [AuthGuardService] },
- { path: 'login', component: LoginComponent },
- { path: 'hosts', component: HostsComponent, canActivate: [AuthGuardService] },
- { path: 'rgw/501/:message', component: Rgw501Component, canActivate: [AuthGuardService] },
+ // Cluster
+ {
+ path: 'hosts',
+ component: HostsComponent,
+ canActivate: [AuthGuardService],
+ data: { breadcrumbs: 'Cluster/Hosts' }
+ },
+ {
+ path: 'monitor',
+ component: MonitorComponent,
+ canActivate: [AuthGuardService],
+ data: { breadcrumbs: 'Cluster/Monitors' }
+ },
+ {
+ path: 'osd',
+ component: OsdListComponent,
+ canActivate: [AuthGuardService],
+ data: { breadcrumbs: 'Cluster/OSDs' }
+ },
+ {
+ path: 'configuration',
+ component: ConfigurationComponent,
+ canActivate: [AuthGuardService],
+ data: { breadcrumbs: 'Cluster/Configuration Documentation' }
+ },
+ {
+ path: 'perf_counters/:type/:id',
+ component: PerformanceCounterComponent,
+ canActivate: [AuthGuardService],
+ data: {
+ breadcrumbs: PerformanceCounterBreadcrumbsResolver
+ }
+ },
+ // Pools
+ {
+ path: 'pool',
+ component: PoolListComponent,
+ canActivate: [AuthGuardService],
+ data: { breadcrumbs: 'Pools' }
+ },
+ // Block
+ {
+ path: 'block',
+ canActivateChild: [AuthGuardService],
+ canActivate: [AuthGuardService],
+ data: { breadcrumbs: true, text: 'Block', path: null },
+ children: [
+ {
+ path: 'rbd',
+ data: { breadcrumbs: 'Images' },
+ children: [
+ { path: '', component: RbdListComponent },
+ { path: 'add', component: RbdFormComponent, data: { breadcrumbs: 'Add' } },
+ { path: 'edit/:pool/:name', component: RbdFormComponent, data: { breadcrumbs: 'Edit' } },
+ {
+ path: 'clone/:pool/:name/:snap',
+ component: RbdFormComponent,
+ data: { breadcrumbs: 'Clone' }
+ },
+ { path: 'copy/:pool/:name', component: RbdFormComponent, data: { breadcrumbs: 'Copy' } },
+ {
+ path: 'copy/:pool/:name/:snap',
+ component: RbdFormComponent,
+ data: { breadcrumbs: 'Copy' }
+ }
+ ]
+ },
+ {
+ path: 'mirroring',
+ component: MirroringComponent,
+ data: { breadcrumbs: 'Mirroring' }
+ },
+ { path: 'iscsi', component: IscsiComponent, data: { breadcrumbs: 'iSCSI' } }
+ ]
+ },
+ // Filesystems
+ {
+ path: 'cephfs',
+ component: CephfsListComponent,
+ canActivate: [AuthGuardService],
+ data: { breadcrumbs: 'Filesystems' }
+ },
+ // Object Gateway
+ {
+ path: 'rgw/501/:message',
+ component: Rgw501Component,
+ canActivate: [AuthGuardService],
+ data: { breadcrumbs: 'Object Gateway' }
+ },
{
path: 'rgw',
- canActivateChild: [ModuleStatusGuardService],
+ canActivateChild: [ModuleStatusGuardService, AuthGuardService],
data: {
moduleStatusGuardConfig: {
apiPath: 'rgw',
redirectTo: 'rgw/501'
- }
+ },
+ breadcrumbs: true,
+ text: 'Object Gateway',
+ path: null
},
children: [
- {
- path: 'daemon',
- component: RgwDaemonListComponent,
- canActivate: [AuthGuardService]
- },
+ { path: 'daemon', component: RgwDaemonListComponent, data: { breadcrumbs: 'Daemons' } },
{
path: 'user',
- component: RgwUserListComponent,
- canActivate: [AuthGuardService]
- },
- {
- path: 'user/add',
- component: RgwUserFormComponent,
- canActivate: [AuthGuardService]
- },
- {
- path: 'user/edit/:uid',
- component: RgwUserFormComponent,
- canActivate: [AuthGuardService]
+ data: { breadcrumbs: 'Users' },
+ children: [
+ { path: '', component: RgwUserListComponent },
+ { path: 'add', component: RgwUserFormComponent, data: { breadcrumbs: 'Add' } },
+ { path: 'edit/:uid', component: RgwUserFormComponent, data: { breadcrumbs: 'Edit' } }
+ ]
},
{
path: 'bucket',
- component: RgwBucketListComponent,
- canActivate: [AuthGuardService]
- },
- {
- path: 'bucket/add',
- component: RgwBucketFormComponent,
- canActivate: [AuthGuardService]
- },
- {
- path: 'bucket/edit/:bucket',
- component: RgwBucketFormComponent,
- canActivate: [AuthGuardService]
+ data: { breadcrumbs: 'Buckets' },
+ children: [
+ { path: '', component: RgwBucketListComponent },
+ { path: 'add', component: RgwBucketFormComponent, data: { breadcrumbs: 'Add' } },
+ { path: 'edit/:bucket', component: RgwBucketFormComponent, data: { breadcrumbs: 'Edit' } }
+ ]
}
]
},
- { path: 'block/iscsi', component: IscsiComponent, canActivate: [AuthGuardService] },
- { path: 'block/rbd', component: RbdListComponent, canActivate: [AuthGuardService] },
- { path: 'rbd/add', component: RbdFormComponent, canActivate: [AuthGuardService] },
- { path: 'rbd/edit/:pool/:name', component: RbdFormComponent, canActivate: [AuthGuardService] },
- { path: 'pool', component: PoolListComponent, canActivate: [AuthGuardService] },
- {
- path: 'rbd/clone/:pool/:name/:snap',
- component: RbdFormComponent,
- canActivate: [AuthGuardService]
- },
+ // Administration
{
- path: 'rbd/copy/:pool/:name',
- component: RbdFormComponent,
- canActivate: [AuthGuardService]
- },
- {
- path: 'rbd/copy/:pool/:name/:snap',
- component: RbdFormComponent,
- canActivate: [AuthGuardService]
- },
- {
- path: 'perf_counters/:type/:id',
- component: PerformanceCounterComponent,
- canActivate: [AuthGuardService]
+ path: 'users',
+ canActivate: [AuthGuardService],
+ canActivateChild: [AuthGuardService],
+ data: { breadcrumbs: 'Administration/Users' },
+ children: [
+ { path: '', component: UserListComponent },
+ { path: 'add', component: UserFormComponent, data: { breadcrumbs: 'Add' } },
+ { path: 'edit/:username', component: UserFormComponent, data: { breadcrumbs: 'Edit' } }
+ ]
},
- { path: 'monitor', component: MonitorComponent, canActivate: [AuthGuardService] },
- { path: 'cephfs', component: CephfsListComponent, canActivate: [AuthGuardService] },
- { path: 'configuration', component: ConfigurationComponent, canActivate: [AuthGuardService] },
- { path: 'mirroring', component: MirroringComponent, canActivate: [AuthGuardService] },
- { path: 'users', component: UserListComponent, canActivate: [AuthGuardService] },
- { path: 'users/add', component: UserFormComponent, canActivate: [AuthGuardService] },
- { path: 'users/edit/:username', component: UserFormComponent, canActivate: [AuthGuardService] },
+ // System
+ { path: 'login', component: LoginComponent },
{ path: '403', component: ForbiddenComponent },
{ path: '404', component: NotFoundComponent },
- { path: 'osd', component: OsdListComponent, canActivate: [AuthGuardService] },
{ path: '**', redirectTo: '/404' }
];
@NgModule({
imports: [RouterModule.forRoot(routes, { useHash: true })],
- exports: [RouterModule]
+ exports: [RouterModule],
+ providers: [PerformanceCounterBreadcrumbsResolver]
})
export class AppRoutingModule {}
<cd-navigation *ngIf="!isLoginActive()"></cd-navigation>
<div class="container-fluid"
[ngClass]="{'full-height':isLoginActive()}">
+ <cd-breadcrumbs></cd-breadcrumbs>
<router-outlet></router-outlet>
</div>
-<nav aria-label="breadcrumb">
- <ol class="breadcrumb">
- <li i18n
- class="breadcrumb-item">Block</li>
- <li i18n
- class="breadcrumb-item active"
- aria-current="page">iSCSI</li>
- </ol>
-</nav>
-
<legend i18n>Daemons</legend>
<cd-table [data]="daemons"
(fetchData)="refresh()"
-<nav aria-label="breadcrumb">
- <ol class="breadcrumb">
- <li class="breadcrumb-item" i18n>Block</li>
- <li class="breadcrumb-item active"
- aria-current="page" i18n>Mirroring</li>
- </ol>
-</nav>
-
<cd-view-cache [status]="status"></cd-view-cache>
<div class="row">
-<nav aria-label="breadcrumb">
- <ol class="breadcrumb">
- <li class="breadcrumb-item">Block</li>
- <li class="breadcrumb-item">
- <a routerLink="/block/rbd">Images</a></li>
- <li class="breadcrumb-item active"
- i18n>{mode, select, editing {Edit} cloning {Clone} copying {Copy} other {Add}}</li>
- </ol>
-</nav>
-
<div class="col-sm-12 col-lg-6">
<form name="rbdForm"
class="form-horizontal"
}
ngOnInit() {
- if (this.router.url.startsWith('/rbd/edit')) {
+ if (this.router.url.startsWith('/block/rbd/edit')) {
this.mode = this.rbdFormMode.editing;
this.disableForEdit();
- } else if (this.router.url.startsWith('/rbd/clone')) {
+ } else if (this.router.url.startsWith('/block/rbd/clone')) {
this.mode = this.rbdFormMode.cloning;
this.disableForClone();
- } else if (this.router.url.startsWith('/rbd/copy')) {
+ } else if (this.router.url.startsWith('/block/rbd/copy')) {
this.mode = this.rbdFormMode.copying;
this.disableForCopy();
}
-<nav aria-label="breadcrumb">
- <ol class="breadcrumb">
- <li i18n
- class="breadcrumb-item">Block</li>
- <li i18n
- class="breadcrumb-item active"
- aria-current="page">Images</li>
- </ol>
-</nav>
-
<cd-view-cache *ngFor="let viewCacheStatus of viewCacheStatusList"
[status]="viewCacheStatus.status"
[statusFor]="viewCacheStatus.statusFor"></cd-view-cache>
<button type="button"
class="btn btn-sm btn-primary"
*ngIf="permission.create && (!permission.update || !selection.hasSingleSelection)"
- routerLink="/rbd/add">
+ routerLink="/block/rbd/add">
<i class="fa fa-fw fa-plus"></i><span i18n>Add</span>
</button>
<button type="button"
class="btn btn-sm btn-primary"
*ngIf="permission.update && (!permission.create || permission.create && selection.hasSingleSelection)"
[ngClass]="{'disabled': !selection.hasSingleSelection || selection.first().executing}"
- routerLink="/rbd/edit/{{ selection.first()?.pool_name | encodeUri }}/{{ selection.first()?.name | encodeUri }}">
+ routerLink="/block/rbd/edit/{{ selection.first()?.pool_name | encodeUri }}/{{ selection.first()?.name | encodeUri }}">
<i class="fa fa-fw fa-pencil"></i>
<span i18n>Edit</span>
</button>
<li role="menuitem"
*ngIf="permission.create">
<a class="dropdown-item"
- routerLink="/rbd/add">
+ routerLink="/block/rbd/add">
<i class="fa fa-fw fa-plus"></i>
<span i18n>Add</span>
</a>
*ngIf="permission.update"
[ngClass]="{'disabled': !selection.hasSingleSelection || selection.first().executing}">
<a class="dropdown-item"
- routerLink="/rbd/edit/{{ selection.first()?.pool_name | encodeUri }}/{{ selection.first()?.name | encodeUri }}">
+ routerLink="/block/rbd/edit/{{ selection.first()?.pool_name | encodeUri }}/{{ selection.first()?.name | encodeUri }}">
<i class="fa fa-fw fa-pencil"></i>
<span i18n>Edit</span>
</a>
*ngIf="permission.create"
[ngClass]="{'disabled': !selection.hasSingleSelection || selection.first().executing}">
<a class="dropdown-item"
- routerLink="/rbd/copy/{{ selection.first()?.pool_name | encodeUri }}/{{ selection.first()?.name | encodeUri }}">
+ routerLink="/block/rbd/copy/{{ selection.first()?.pool_name | encodeUri }}/{{ selection.first()?.name | encodeUri }}">
<i class="fa fa-fw fa-copy"></i>
<span i18n>Copy</span>
</a>
*ngIf="permission.create"
[ngClass]="{'disabled': !selection.hasSingleSelection || selection.first().executing}">
<a class="dropdown-item"
- routerLink="/rbd/clone/{{ poolName | encodeUri }}/{{ rbdName | encodeUri }}/{{ selection.first()?.name | encodeUri }}">
+ routerLink="/block/rbd/clone/{{ poolName | encodeUri }}/{{ rbdName | encodeUri }}/{{ selection.first()?.name | encodeUri }}">
<i class="fa fa-fw fa-clone"></i>
<span i18n>Clone</span>
</a>
*ngIf="permission.create"
[ngClass]="{'disabled': !selection.hasSingleSelection || selection.first().executing}">
<a class="dropdown-item"
- routerLink="/rbd/copy/{{ poolName | encodeUri }}/{{ rbdName | encodeUri }}/{{ selection.first()?.name | encodeUri }}">
+ routerLink="/block/rbd/copy/{{ poolName | encodeUri }}/{{ rbdName | encodeUri }}/{{ selection.first()?.name | encodeUri }}">
<i class="fa fa-fw fa-copy"></i>
<span i18n>Copy</span>
</a>
-<nav aria-label="breadcrumb">
- <ol class="breadcrumb">
- <li i18n
- class="breadcrumb-item active">Filesystems</li>
- </ol>
-</nav>
-
<cd-table [data]="filesystems"
columnMode="flex"
[columns]="columns"
-<nav aria-label="breadcrumb">
- <ol class="breadcrumb">
- <li class="breadcrumb-item">Cluster</li>
- <li class="breadcrumb-item active"
- aria-current="page">Configuration Documentation</li>
- </ol>
-</nav>
<cd-table [data]="data | filter:filters"
(fetchData)="getConfigurationList($event)"
[columns]="columns"
-<nav aria-label="breadcrumb">
- <ol class="breadcrumb">
- <li i18n
- class="breadcrumb-item">Cluster</li>
- <li i18n
- class="breadcrumb-item active"
- aria-current="page">Hosts</li>
- </ol>
-</nav>
<cd-table [data]="hosts"
[columns]="columns"
columnMode="flex"
<ng-template #servicesTpl let-value="value">
<span *ngFor="let service of value; last as isLast">
<a [routerLink]="[service.cdLink]"
+ [queryParams]="cdParams"
*ngIf="service.canRead">{{ service.type }}.{{ service.id }}</a>
<span *ngIf="!service.canRead">{{ service.type }}.{{ service.id }}</span>
{{ !isLast ? ", " : "" }}
columns: Array<CdTableColumn> = [];
hosts: Array<object> = [];
isLoadingHosts = false;
+ cdParams = { fromLink: '/hosts' };
@ViewChild('servicesTpl') public servicesTpl: TemplateRef<any>;
-<nav aria-label="breadcrumb">
- <ol class="breadcrumb">
- <li i18n
- class="breadcrumb-item">Cluster</li>
- <li i18n
- class="breadcrumb-item active"
- aria-current="page">Monitors</li>
- </ol>
-</nav>
-
<div class="row">
<div class="col-md-4">
<fieldset>
-<nav aria-label="breadcrumb">
- <ol class="breadcrumb">
- <li class="breadcrumb-item">Cluster</li>
- <li class="breadcrumb-item active">OSDs</li>
- </ol>
-</nav>
<cd-table [data]="osds"
(fetchData)="getOsdList()"
[columns]="columns"
-<nav aria-label="breadcrumb">
- <ol class="breadcrumb">
- <li class="breadcrumb-item">Cluster</li>
- <li class="breadcrumb-item">
- <a [routerLink]="fromLink">
- <span *ngIf="fromLink === '/monitor'">Monitors</span>
- <span *ngIf="fromLink === '/hosts'">Hosts</span>
- </a>
- </li>
- <li class="breadcrumb-item active"
- i18n>Performance Counters</li>
- </ol>
-</nav>
-
<fieldset>
<legend>{{ serviceType }}.{{ serviceId }}</legend>
<cd-table-performance-counter [serviceType]="serviceType"
-<nav aria-label="breadcrumb">
- <ol class="breadcrumb">
- <li class="breadcrumb-item active">Pools</li>
- </ol>
-</nav>
<cd-table [data]="pools"
(fetchData)="getPoolList($event)"
[columns]="columns"
-<nav aria-label="breadcrumb">
- <ol class="breadcrumb">
- <li i18n
- class="breadcrumb-item">Object Gateway</li>
- </ol>
-</nav>
<cd-info-panel>
{{ message }}
<ng-container i18n>
-<nav aria-label="breadcrumb">
- <ol class="breadcrumb">
- <li class="breadcrumb-item"
- i18n>Object Gateway</li>
- <li class="breadcrumb-item">
- <a routerLink="/rgw/bucket"
- i18n>Buckets</a>
- </li>
- <li class="breadcrumb-item active"
- aria-current="page"
- i18n>
- {editing, select, 1 {Edit} other {Add}}
- </li>
- </ol>
-</nav>
-
<cd-loading-panel *ngIf="editing && loading && !error"
i18n>
Loading bucket data...
-<nav aria-label="breadcrumb">
- <ol class="breadcrumb">
- <li i18n
- class="breadcrumb-item">Object Gateway</li>
- <li i18n
- class="breadcrumb-item active"
- aria-current="page">Buckets</li>
- </ol>
-</nav>
<cd-table #table
[autoReload]="false"
[data]="buckets"
-<nav aria-label="breadcrumb">
- <ol class="breadcrumb">
- <li i18n
- class="breadcrumb-item">Object Gateway</li>
- <li i18n
- class="breadcrumb-item active"
- aria-current="page">Daemons</li>
- </ol>
-</nav>
-
<cd-table [data]="daemons"
[columns]="columns"
columnMode="flex"
-<nav aria-label="breadcrumb">
- <ol class="breadcrumb">
- <li class="breadcrumb-item"
- i18n>Object Gateway</li>
- <li class="breadcrumb-item">
- <a routerLink="/rgw/user"
- i18n>Users</a>
- </li>
- <li class="breadcrumb-item active"
- aria-current="page"
- i18n>
- {editing, select, 1 {Edit} other {Add}}
- </li>
- </ol>
-</nav>
-
<cd-loading-panel *ngIf="editing && loading && !error"
i18n>
Loading user data...
-<nav aria-label="breadcrumb">
- <ol class="breadcrumb">
- <li i18n
- class="breadcrumb-item">Object Gateway</li>
- <li i18n
- class="breadcrumb-item active"
- aria-current="page">Users</li>
- </ol>
-</nav>
<cd-table #table
[autoReload]="false"
[data]="users"
-<nav aria-label="breadcrumb">
- <ol class="breadcrumb">
- <li class="breadcrumb-item">Administration</li>
- <li class="breadcrumb-item">
- <a routerLink="/users">Users</a></li>
- <li class="breadcrumb-item active"
- i18n>{mode, select, editing {Edit} other {Add}}</li>
- </ol>
-</nav>
-
<div class="col-sm-12 col-lg-6">
<form name="userForm"
class="form-horizontal"
-<nav aria-label="breadcrumb">
- <ol class="breadcrumb">
- <li i18n
- class="breadcrumb-item">Administration</li>
- <li i18n
- class="breadcrumb-item active"
- aria-current="page">Users</li>
- </ol>
-</nav>
-
<cd-table [data]="users"
columnMode="flex"
[columns]="columns"
--- /dev/null
+<ol *ngIf="crumbs.length"
+ class="breadcrumb">
+ <li *ngFor="let crumb of crumbs; let last = last"
+ [ngClass]="{ 'active': last }"
+ class="breadcrumb-item">
+ <a *ngIf="!last && crumb.path !== null"
+ [routerLink]="crumb.path">{{ crumb.text }}</a>
+ <span *ngIf="last || crumb.path === null">{{ crumb.text }}</span>
+ </li>
+</ol>
--- /dev/null
+@import '../../../../defaults';
+
+.breadcrumb {
+ padding: 8px 0;
+ background-color: transparent;
+ border-radius: 0;
+}
+
+.breadcrumb > li + li:before {
+ padding: 0 5px 0 7px;
+ color: $color-breadcrumb;
+ font-family: 'ForkAwesome';
+ content: '\f101';
+}
+
+.breadcrumb > li > span {
+ color: $color-breadcrumb;
+}
--- /dev/null
+import { CommonModule } from '@angular/common';
+import { Component } from '@angular/core';
+import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { Router, Routes } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { configureTestBed } from '../../../../testing/unit-test-helper';
+import { PerformanceCounterBreadcrumbsResolver } from '../../../app-routing.module';
+import { BreadcrumbsComponent } from './breadcrumbs.component';
+
+describe('BreadcrumbsComponent', () => {
+ let component: BreadcrumbsComponent;
+ let fixture: ComponentFixture<BreadcrumbsComponent>;
+ let router: Router;
+
+ @Component({ selector: 'cd-fake', template: '' })
+ class FakeComponent {}
+
+ const routes: Routes = [
+ {
+ path: 'hosts',
+ component: FakeComponent,
+ data: { breadcrumbs: 'Cluster/Hosts' }
+ },
+ {
+ path: 'perf_counters',
+ component: FakeComponent,
+ data: {
+ breadcrumbs: PerformanceCounterBreadcrumbsResolver
+ }
+ },
+ {
+ path: 'block',
+ data: { breadcrumbs: true, text: 'Block', path: null },
+ children: [
+ {
+ path: 'rbd',
+ data: { breadcrumbs: 'Images' },
+ children: [
+ { path: '', component: FakeComponent },
+ { path: 'add', component: FakeComponent, data: { breadcrumbs: 'Add' } }
+ ]
+ }
+ ]
+ }
+ ];
+
+ configureTestBed({
+ declarations: [BreadcrumbsComponent, FakeComponent],
+ imports: [CommonModule, RouterTestingModule.withRoutes(routes)],
+ providers: [PerformanceCounterBreadcrumbsResolver]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(BreadcrumbsComponent);
+ router = TestBed.get(Router);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ expect(component.crumbs).toEqual([]);
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ expect(component.subscription).toBeDefined();
+ });
+
+ it(
+ 'should run postProcess and split the breadcrumbs when navigating to hosts',
+ fakeAsync(() => {
+ router.navigateByUrl('/hosts');
+ tick();
+ expect(component.crumbs).toEqual([
+ { path: null, text: 'Cluster' },
+ { path: '/hosts', text: 'Hosts' }
+ ]);
+ })
+ );
+
+ it(
+ 'should display empty breadcrumb when navigating to perf_counters from unknown path',
+ fakeAsync(() => {
+ router.navigateByUrl('/perf_counters');
+ tick();
+ expect(component.crumbs).toEqual([
+ { path: null, text: 'Cluster' },
+ { path: null, text: '' },
+ { path: '', text: 'Performance Counters' }
+ ]);
+ })
+ );
+
+ it(
+ 'should display Monitor breadcrumb when navigating to perf_counters from Monitors',
+ fakeAsync(() => {
+ router.navigate(['/perf_counters'], { queryParams: { fromLink: '/monitor' } });
+ tick();
+ expect(component.crumbs).toEqual([
+ { path: null, text: 'Cluster' },
+ { path: '/monitor', text: 'Monitors' },
+ { path: '', text: 'Performance Counters' }
+ ]);
+ })
+ );
+
+ it(
+ 'should display Hosts breadcrumb when navigating to perf_counters from Hosts',
+ fakeAsync(() => {
+ router.navigate(['/perf_counters'], { queryParams: { fromLink: '/hosts' } });
+ tick();
+ expect(component.crumbs).toEqual([
+ { path: null, text: 'Cluster' },
+ { path: '/hosts', text: 'Hosts' },
+ { path: '', text: 'Performance Counters' }
+ ]);
+ })
+ );
+
+ it(
+ 'should show all 3 breadcrumbs when navigating to RBD Add',
+ fakeAsync(() => {
+ router.navigateByUrl('/block/rbd/add');
+ tick();
+ expect(component.crumbs).toEqual([
+ { path: null, text: 'Block' },
+ { path: '/block/rbd', text: 'Images' },
+ { path: '/block/rbd/add', text: 'Add' }
+ ]);
+ })
+ );
+
+ it('should unsubscribe on ngOnDestroy', () => {
+ expect(component.subscription.closed).toBeFalsy();
+ component.ngOnDestroy();
+ expect(component.subscription.closed).toBeTruthy();
+ });
+});
--- /dev/null
+/*
+The MIT License
+
+Copyright (c) 2017 (null) McNull https://github.com/McNull
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+ */
+
+import { Component, Injector, OnDestroy } from '@angular/core';
+import { ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router';
+
+import { from, Observable, of, Subscription } from 'rxjs';
+import { concat, distinct, filter, first, flatMap, toArray } from 'rxjs/operators';
+
+import { BreadcrumbsResolver, IBreadcrumb } from '../../../shared/models/breadcrumbs';
+
+@Component({
+ selector: 'cd-breadcrumbs',
+ templateUrl: './breadcrumbs.component.html',
+ styleUrls: ['./breadcrumbs.component.scss']
+})
+export class BreadcrumbsComponent implements OnDestroy {
+ crumbs: IBreadcrumb[] = [];
+ subscription: Subscription;
+ private defaultResolver = new BreadcrumbsResolver();
+
+ constructor(private router: Router, private injector: Injector) {
+ this.subscription = this.router.events
+ .pipe(filter((x) => x instanceof NavigationEnd))
+ .subscribe(() => {
+ const currentRoot = router.routerState.snapshot.root;
+
+ this._resolveCrumbs(currentRoot)
+ .pipe(
+ flatMap((x) => x),
+ distinct((x) => x.text),
+ toArray(),
+ flatMap((x) => {
+ const y = this.postProcess(x);
+ return this.wrapIntoObservable<IBreadcrumb[]>(y).pipe(first());
+ })
+ )
+ .subscribe((x) => {
+ this.crumbs = x;
+ });
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.subscription.unsubscribe();
+ }
+
+ private _resolveCrumbs(route: ActivatedRouteSnapshot): Observable<IBreadcrumb[]> {
+ let crumbs$: Observable<IBreadcrumb[]>;
+
+ const data = route.routeConfig && route.routeConfig.data;
+
+ if (data && data.breadcrumbs) {
+ let resolver: BreadcrumbsResolver;
+
+ if (data.breadcrumbs.prototype instanceof BreadcrumbsResolver) {
+ resolver = this.injector.get(data.breadcrumbs);
+ } else {
+ resolver = this.defaultResolver;
+ }
+
+ const result = resolver.resolve(route);
+ crumbs$ = this.wrapIntoObservable<IBreadcrumb[]>(result).pipe(first());
+ } else {
+ crumbs$ = of([]);
+ }
+
+ if (route.firstChild) {
+ crumbs$ = crumbs$.pipe(concat(this._resolveCrumbs(route.firstChild)));
+ }
+
+ return crumbs$;
+ }
+
+ postProcess(breadcrumbs: IBreadcrumb[]) {
+ const result = [];
+ breadcrumbs.forEach((element) => {
+ const split = element.text.split('/');
+ if (split.length > 1) {
+ element.text = split[split.length - 1];
+ for (let i = 0; i < split.length - 1; i++) {
+ result.push({ text: split[i], path: null });
+ }
+ }
+ result.push(element);
+ });
+ return result;
+ }
+
+ isPromise(value: any): boolean {
+ return value && typeof value.then === 'function';
+ }
+
+ wrapIntoObservable<T>(value: T | Promise<T> | Observable<T>): Observable<T> {
+ if (value instanceof Observable) {
+ return value;
+ }
+
+ if (this.isPromise(value)) {
+ return from(Promise.resolve(value));
+ }
+
+ return of(value as T);
+ }
+}
import { AuthModule } from '../auth/auth.module';
import { AboutComponent } from './about/about.component';
import { AdministrationComponent } from './administration/administration.component';
+import { BreadcrumbsComponent } from './breadcrumbs/breadcrumbs.component';
import { DashboardHelpComponent } from './dashboard-help/dashboard-help.component';
import { NavigationComponent } from './navigation/navigation.component';
import { NotificationsComponent } from './notifications/notifications.component';
],
declarations: [
AboutComponent,
+ BreadcrumbsComponent,
NavigationComponent,
NotificationsComponent,
TaskManagerComponent,
DashboardHelpComponent,
AdministrationComponent
],
- exports: [NavigationComponent]
+ exports: [NavigationComponent, BreadcrumbsComponent]
})
export class NavigationModule {}
*ngIf="permissions.rbdMirroring.read">
<a i18n
class="dropdown-item"
- routerLink="/mirroring/"> Mirroring
+ routerLink="/block/mirroring"> Mirroring
<small *ngIf="summaryData?.rbd_mirroring?.warnings !== 0"
class="label label-warning">{{ summaryData?.rbd_mirroring?.warnings }}</small>
<small *ngIf="summaryData?.rbd_mirroring?.errors !== 0"
--- /dev/null
+/*
+The MIT License
+
+Copyright (c) 2017 (null) McNull https://github.com/McNull
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+ */
+
+import { ActivatedRouteSnapshot, Resolve, UrlSegment } from '@angular/router';
+
+import { Observable, of } from 'rxjs';
+
+export class BreadcrumbsResolver implements Resolve<IBreadcrumb[]> {
+ public resolve(
+ route: ActivatedRouteSnapshot
+ ): Observable<IBreadcrumb[]> | Promise<IBreadcrumb[]> | IBreadcrumb[] {
+ const data = route.routeConfig.data;
+ const path = data.path === null ? null : this.getFullPath(route);
+
+ const text =
+ typeof data.breadcrumbs === 'string'
+ ? data.breadcrumbs
+ : data.breadcrumbs.text || data.text || path;
+
+ const crumbs: IBreadcrumb[] = [{ text: text, path: path }];
+
+ return of(crumbs);
+ }
+
+ public getFullPath(route: ActivatedRouteSnapshot): string {
+ const relativePath = (segments: UrlSegment[]) =>
+ segments.reduce((a, v) => (a += '/' + v.path), '');
+ const fullPath = (routes: ActivatedRouteSnapshot[]) =>
+ routes.reduce((a, v) => (a += relativePath(v.url)), '');
+
+ return fullPath(route.pathFromRoot);
+ }
+}
+
+export interface IBreadcrumb {
+ text: string;
+ path: string;
+}
import { Injectable } from '@angular/core';
-import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
+import {
+ ActivatedRouteSnapshot,
+ CanActivate,
+ CanActivateChild,
+ Router,
+ RouterStateSnapshot
+} from '@angular/router';
import { AuthStorageService } from './auth-storage.service';
import { ServicesModule } from './services.module';
@Injectable({
providedIn: ServicesModule
})
-export class AuthGuardService implements CanActivate {
+export class AuthGuardService implements CanActivate, CanActivateChild {
constructor(private router: Router, private authStorageService: AuthStorageService) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
this.router.navigate(['/login']);
return false;
}
+
+ canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
+ return this.canActivate(route, state);
+ }
}
.text-monospace {
font-family: monospace;
}
-/* Breadcrumb */
-.breadcrumb {
- padding: 8px 0;
- background-color: transparent;
- border-radius: 0;
-}
-.breadcrumb > li + li:before {
- padding: 0 5px 0 7px;
- color: $color-breadcrumb;
- font-family: 'ForkAwesome';
- content: '\f101';
-}
-.breadcrumb > li > span {
- color: $color-breadcrumb;
-}
+
/* Buttons */
.btn-primary {
color: $color-button-text;