]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Add breadcrumbs component 23414/head
authorTiago Melo <tspmelo@gmail.com>
Fri, 3 Aug 2018 13:02:46 +0000 (14:02 +0100)
committerTiago Melo <tspmelo@gmail.com>
Mon, 6 Aug 2018 13:23:43 +0000 (14:23 +0100)
Now we don't need to add a breadcrumb in each new page we create,
just add the necessary extra data in the route module and the
breadcrumb will be automatically created.

I used a modified version of ngx-breadcrumbs from McNull:
https://github.com/McNull/ngx-breadcrumbs

Fixes: http://tracker.ceph.com/issues/24781
Signed-off-by: Tiago Melo <tmelo@suse.com>
33 files changed:
src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts
src/pybind/mgr/dashboard/frontend/src/app/app.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/mirroring.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/hosts/hosts.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/performance-counter/performance-counter/performance-counter.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-501/rgw-501.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-list/rgw-bucket-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-form/rgw-user-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-list/rgw-user-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-form/user-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation.module.ts
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/models/breadcrumbs.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-guard.service.ts
src/pybind/mgr/dashboard/frontend/src/styles.scss

index 0d44e19ec7d9eb0cc4377b180a7d05cf80cc6307..b8a6df65dae6bd4c121fa76c57a7dff604f67464 100644 (file)
@@ -1,5 +1,5 @@
 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';
@@ -24,103 +24,181 @@ import { UserFormComponent } from './core/auth/user-form/user-form.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 {}
index 638edaa604f1ad149d0b81aa12a54c2bf4635fd3..669222afc80e7394af25cb7dee0a3f75214b4016 100644 (file)
@@ -1,5 +1,6 @@
 <cd-navigation *ngIf="!isLoginActive()"></cd-navigation>
 <div class="container-fluid"
      [ngClass]="{'full-height':isLoginActive()}">
+  <cd-breadcrumbs></cd-breadcrumbs>
   <router-outlet></router-outlet>
 </div>
index 68f9326690cd76411c0204d5400857f8aace16f0..b2b42a65254a42871592841d58f0bb8e31e8bf8b 100644 (file)
@@ -1,13 +1,3 @@
-<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()"
index a76047d431f666924e541e34876cf1b611fda903..0c17084f9abff15fb0e420f87f01b5d56bf295a0 100644 (file)
@@ -1,11 +1,3 @@
-<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">
index 6abe5603d5c6433503eab60e205970a18c5727c1..d03f3edfeff438378b731af2cb9f12a6781af843 100644 (file)
@@ -1,13 +1,3 @@
-<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"
index 0d4b8785cf42d4bb33238df56973b7e8b430318b..6bac066e155e637b8d72583bb8a4e9f54f4f8306 100644 (file)
@@ -191,13 +191,13 @@ export class RbdFormComponent implements OnInit {
   }
 
   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();
     }
index 2ca8b9c58569c4013f0d9cc77dfd8e34134d94d7..d594fdb816d4ac3e3ece1a1030b8438e9f190085 100644 (file)
@@ -1,13 +1,3 @@
-<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>
@@ -57,7 +47,7 @@
         <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>
@@ -66,7 +56,7 @@
             *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>
@@ -75,7 +65,7 @@
             *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>
index a742f216944739fd852f9e3a0656add9e9520668..7ad626865d16686c11215fe08c33852ae1151bce 100644 (file)
@@ -73,7 +73,7 @@
             *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>
@@ -82,7 +82,7 @@
             *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>
index 726deba82d287864cfe2db166dc79eab39373d21..b3fecb3eca39b56621a78c390024f22b9461538e 100644 (file)
@@ -1,10 +1,3 @@
-<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"
index f0255f6a2f491e54b1fdf17c304ebce484532695..7b3aeb3aa4ca5f7e7534686ac5f33b23f81569e8 100644 (file)
@@ -1,10 +1,3 @@
-<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"
index 192f060329fed0b98f9e792a70088fa1df0288c0..4f71cad08cbe98da8a1e9e147c0c64e90869f4f8 100644 (file)
@@ -1,12 +1,3 @@
-<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"
@@ -14,6 +5,7 @@
   <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 ? ", " : "" }}
index f574055bded6330d4c4c04266d6cec2a6acf2a1a..630b4f35d1db85a586b05c2688b4c9d1e1f222f6 100644 (file)
@@ -17,6 +17,7 @@ export class HostsComponent implements OnInit {
   columns: Array<CdTableColumn> = [];
   hosts: Array<object> = [];
   isLoadingHosts = false;
+  cdParams = { fromLink: '/hosts' };
 
   @ViewChild('servicesTpl') public servicesTpl: TemplateRef<any>;
 
index d59de84c1451215ebf371d7631d392f0fa1f85ac..fd4ce6787ffff70c78a5d507a216227f288e4c18 100644 (file)
@@ -1,13 +1,3 @@
-<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>
index 733bcf26fb251f299dc8999ee7b8b3722bf106aa..1381ad56033dc163d875749de7fa49bcf907edcf 100644 (file)
@@ -1,9 +1,3 @@
-<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"
index 615228b7e68b29a8da864607aa49611946a3d699..87e4bfe0d5c60aa70b0202162bcc216d9e6ecfbc 100644 (file)
@@ -1,17 +1,3 @@
-<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"
index 96f4442bfe9e166eb476c04fa3b91a31d40a9242..440c4caf1bd168d926444446d82c477554f75061 100644 (file)
@@ -1,8 +1,3 @@
-<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"
index 4f03611eaeeee8864d0099233ca07b987bcbfaa9..471bf738b7096f30376c88606f000cc5b464eba6 100644 (file)
@@ -1,9 +1,3 @@
-<nav aria-label="breadcrumb">
-  <ol class="breadcrumb">
-    <li i18n
-        class="breadcrumb-item">Object Gateway</li>
-  </ol>
-</nav>
 <cd-info-panel>
   {{ message }}
   <ng-container i18n>
index f0e4d163260d0e54c072a5964255c3dbceb44cb3..4bd688756b3139f77b645198f7f76a128978fd7c 100644 (file)
@@ -1,19 +1,3 @@
-<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...
index 0f6afe3eff225207f99c120ef3560172c63d1883..b0ab16540cea77e51a03e6bd81f853a841db030c 100644 (file)
@@ -1,12 +1,3 @@
-<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"
index c93bef352bd8dcaf27c82ce5b423b94fea7800cd..35cdf214951d8a603d06c773b1c5dd7420046419 100644 (file)
@@ -1,13 +1,3 @@
-<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"
index 873963b9ab7d5e6f30d143af9adbf33786fb7c2a..8b46cea8ca92ea3d20c0abfd20a4438fd068cdd2 100644 (file)
@@ -1,19 +1,3 @@
-<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...
index 3cb0bfd83582a073cadbd52d4cca93e6b7688278..898799f3007b14e2158747e9bf180363bef48fd7 100644 (file)
@@ -1,12 +1,3 @@
-<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"
index 60dd162dc47eed862851851a57067ff5d6a31a2c..5bfa2fc8bd50f9cf8c4463ffdf3605639e9a697c 100644 (file)
@@ -1,13 +1,3 @@
-<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"
index f2e902cca3828f83b6583cbcc95875b42f3cee8b..e4368fd94add11f2f8811a957a65ea2ed544b438 100644 (file)
@@ -1,13 +1,3 @@
-<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"
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.html
new file mode 100644 (file)
index 0000000..3ca38d9
--- /dev/null
@@ -0,0 +1,10 @@
+<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>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.scss
new file mode 100644 (file)
index 0000000..a8dbf5e
--- /dev/null
@@ -0,0 +1,18 @@
+@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;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.spec.ts
new file mode 100644 (file)
index 0000000..9b7dac3
--- /dev/null
@@ -0,0 +1,136 @@
+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();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.ts
new file mode 100644 (file)
index 0000000..f7b9d8a
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+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);
+  }
+}
index 178eeade9b7ac6c295e6d59c809b6d2baa3a795d..f35e78fd7337b2238f52612b5dbd57493e175f94 100644 (file)
@@ -9,6 +9,7 @@ import { SharedModule } from '../../shared/shared.module';
 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';
@@ -29,12 +30,13 @@ import { TaskManagerComponent } from './task-manager/task-manager.component';
   ],
   declarations: [
     AboutComponent,
+    BreadcrumbsComponent,
     NavigationComponent,
     NotificationsComponent,
     TaskManagerComponent,
     DashboardHelpComponent,
     AdministrationComponent
   ],
-  exports: [NavigationComponent]
+  exports: [NavigationComponent, BreadcrumbsComponent]
 })
 export class NavigationModule {}
index d212e21deea94992f0f63384ae6913942144617c..beae9ebf906222517d1cb66de34c967990bf5687 100644 (file)
               *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"
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/breadcrumbs.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/breadcrumbs.ts
new file mode 100644 (file)
index 0000000..10e7999
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+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;
+}
index 398910a014d087ed6cfb769dde263905c4324ed9..f376e6a8cab9e1a84bc5a7d16ca0941bbffdacc8 100644 (file)
@@ -1,5 +1,11 @@
 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';
@@ -7,7 +13,7 @@ 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) {
@@ -17,4 +23,8 @@ export class AuthGuardService implements CanActivate {
     this.router.navigate(['/login']);
     return false;
   }
+
+  canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
+    return this.canActivate(route, state);
+  }
 }
index 0d05a3162b2a46a16366880061f2b34854d1779f..a4b5cd474cb7ce01ddfa5305fe90c5febd4452ea 100644 (file)
@@ -80,21 +80,7 @@ option {
 .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;