]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Introduce layout components
authorVolker Theile <vtheile@suse.com>
Mon, 13 Jan 2020 09:06:25 +0000 (10:06 +0100)
committerVolker Theile <vtheile@suse.com>
Mon, 20 Jan 2020 08:09:54 +0000 (09:09 +0100)
This PR will simplify the code of the app component in that way that it introduces layout components. Depending on the route a specific layout is choosen. Currently there are two layouts:

- Workbench
- Blank

The blank layout, which does not show any navigation controls, is used for the error, login and logout pages. The workbench layout shows the navigation controls and is mainly used for all pages that are used to configure Ceph.

The 403 and 404 pages have a 'Back' button that will redirect to /login. The Angular router will redirect to /dashboard if logged in.

Fixes: https://tracker.ceph.com/issues/43565
Signed-off-by: Volker Theile <vtheile@suse.com>
23 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/app.component.scss
src/pybind/mgr/dashboard/frontend/src/app/app.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/app.component.ts
src/pybind/mgr/dashboard/frontend/src/app/app.module.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.scss
src/pybind/mgr/dashboard/frontend/src/app/core/auth/sso/sso-not-found/sso-not-found.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/core.module.ts
src/pybind/mgr/dashboard/frontend/src/app/core/forbidden/forbidden.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/forbidden/forbidden.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.scss
src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/styles.scss

index 2913b293dc391e37c4c6604aedaa3932b361ca3c..fe7be74abe72c2b77b50b5c4463a3446308619d0 100644 (file)
@@ -28,6 +28,8 @@ import { LoginComponent } from './core/auth/login/login.component';
 import { SsoNotFoundComponent } from './core/auth/sso/sso-not-found/sso-not-found.component';
 import { UserPasswordFormComponent } from './core/auth/user-password-form/user-password-form.component';
 import { ForbiddenComponent } from './core/forbidden/forbidden.component';
+import { BlankLayoutComponent } from './core/layouts/blank-layout/blank-layout.component';
+import { WorkbenchLayoutComponent } from './core/layouts/workbench-layout/workbench-layout.component';
 import { NotFoundComponent } from './core/not-found/not-found.component';
 import { ActionLabels, URLVerbs } from './shared/constants/app.constants';
 import { BreadcrumbsResolver, IBreadcrumb } from './shared/models/breadcrumbs';
@@ -69,237 +71,249 @@ export class StartCaseBreadcrumbsResolver extends BreadcrumbsResolver {
 const routes: Routes = [
   // Dashboard
   { path: '', redirectTo: 'dashboard', pathMatch: 'full' },
-  { path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuardService] },
-  // Cluster
   {
-    path: 'hosts',
-    canActivate: [AuthGuardService],
-    data: { breadcrumbs: 'Cluster/Hosts' },
+    path: '',
+    component: WorkbenchLayoutComponent,
     children: [
-      { path: '', component: HostsComponent },
+      { path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuardService] },
+      // Cluster
       {
-        path: URLVerbs.ADD,
-        component: HostFormComponent,
-        data: { breadcrumbs: ActionLabels.ADD }
-      }
-    ]
-  },
-  {
-    path: 'monitor',
-    component: MonitorComponent,
-    canActivate: [AuthGuardService],
-    data: { breadcrumbs: 'Cluster/Monitors' }
-  },
-  {
-    path: 'services',
-    component: ServicesComponent,
-    canActivate: [AuthGuardService],
-    data: { breadcrumbs: 'Cluster/Services' }
-  },
-  {
-    path: 'inventory',
-    component: InventoryComponent,
-    canActivate: [AuthGuardService],
-    data: { breadcrumbs: 'Cluster/Inventory' }
-  },
-  {
-    path: 'osd',
-    canActivate: [AuthGuardService],
-    canActivateChild: [AuthGuardService],
-    data: { breadcrumbs: 'Cluster/OSDs' },
-    children: [
-      { path: '', component: OsdListComponent },
+        path: 'hosts',
+        canActivate: [AuthGuardService],
+        data: { breadcrumbs: 'Cluster/Hosts' },
+        children: [
+          { path: '', component: HostsComponent },
+          {
+            path: URLVerbs.ADD,
+            component: HostFormComponent,
+            data: { breadcrumbs: ActionLabels.ADD }
+          }
+        ]
+      },
       {
-        path: URLVerbs.CREATE,
-        component: OsdFormComponent,
-        data: { breadcrumbs: ActionLabels.CREATE }
-      }
-    ]
-  },
-  {
-    path: 'configuration',
-    data: { breadcrumbs: 'Cluster/Configuration' },
-    children: [
-      { path: '', component: ConfigurationComponent },
+        path: 'monitor',
+        component: MonitorComponent,
+        canActivate: [AuthGuardService],
+        data: { breadcrumbs: 'Cluster/Monitors' }
+      },
       {
-        path: 'edit/:name',
-        component: ConfigurationFormComponent,
-        data: { breadcrumbs: ActionLabels.EDIT }
-      }
-    ]
-  },
-  {
-    path: 'crush-map',
-    component: CrushmapComponent,
-    canActivate: [AuthGuardService],
-    data: { breadcrumbs: 'Cluster/CRUSH map' }
-  },
-  {
-    path: 'logs',
-    component: LogsComponent,
-    canActivate: [AuthGuardService],
-    data: { breadcrumbs: 'Cluster/Logs' }
-  },
-  {
-    path: 'monitoring',
-    canActivate: [AuthGuardService],
-    data: { breadcrumbs: 'Cluster/Monitoring' },
-    children: [
+        path: 'services',
+        component: ServicesComponent,
+        canActivate: [AuthGuardService],
+        data: { breadcrumbs: 'Cluster/Services' }
+      },
       {
-        path: '',
-        component: MonitoringListComponent
+        path: 'inventory',
+        component: InventoryComponent,
+        canActivate: [AuthGuardService],
+        data: { breadcrumbs: 'Cluster/Inventory' }
       },
       {
-        path: 'silence/' + URLVerbs.CREATE,
-        component: SilenceFormComponent,
-        data: { breadcrumbs: `${ActionLabels.CREATE} Silence` }
+        path: 'osd',
+        canActivate: [AuthGuardService],
+        canActivateChild: [AuthGuardService],
+        data: { breadcrumbs: 'Cluster/OSDs' },
+        children: [
+          { path: '', component: OsdListComponent },
+          {
+            path: URLVerbs.CREATE,
+            component: OsdFormComponent,
+            data: { breadcrumbs: ActionLabels.CREATE }
+          }
+        ]
       },
       {
-        path: `silence/${URLVerbs.CREATE}/:id`,
-        component: SilenceFormComponent,
-        data: { breadcrumbs: ActionLabels.CREATE }
+        path: 'configuration',
+        data: { breadcrumbs: 'Cluster/Configuration' },
+        children: [
+          { path: '', component: ConfigurationComponent },
+          {
+            path: 'edit/:name',
+            component: ConfigurationFormComponent,
+            data: { breadcrumbs: ActionLabels.EDIT }
+          }
+        ]
       },
       {
-        path: `silence/${URLVerbs.EDIT}/:id`,
-        component: SilenceFormComponent,
-        data: { breadcrumbs: ActionLabels.EDIT }
+        path: 'crush-map',
+        component: CrushmapComponent,
+        canActivate: [AuthGuardService],
+        data: { breadcrumbs: 'Cluster/CRUSH map' }
       },
       {
-        path: `silence/${URLVerbs.RECREATE}/:id`,
-        component: SilenceFormComponent,
-        data: { breadcrumbs: ActionLabels.RECREATE }
-      }
-    ]
-  },
-  {
-    path: 'perf_counters/:type/:id',
-    component: PerformanceCounterComponent,
-    canActivate: [AuthGuardService],
-    data: {
-      breadcrumbs: PerformanceCounterBreadcrumbsResolver
-    }
-  },
-  // Mgr modules
-  {
-    path: 'mgr-modules',
-    canActivate: [AuthGuardService],
-    canActivateChild: [AuthGuardService],
-    data: { breadcrumbs: 'Cluster/Manager modules' },
-    children: [
+        path: 'logs',
+        component: LogsComponent,
+        canActivate: [AuthGuardService],
+        data: { breadcrumbs: 'Cluster/Logs' }
+      },
       {
-        path: '',
-        component: MgrModuleListComponent
+        path: 'monitoring',
+        canActivate: [AuthGuardService],
+        data: { breadcrumbs: 'Cluster/Monitoring' },
+        children: [
+          {
+            path: '',
+            component: MonitoringListComponent
+          },
+          {
+            path: 'silence/' + URLVerbs.CREATE,
+            component: SilenceFormComponent,
+            data: { breadcrumbs: `${ActionLabels.CREATE} Silence` }
+          },
+          {
+            path: `silence/${URLVerbs.CREATE}/:id`,
+            component: SilenceFormComponent,
+            data: { breadcrumbs: ActionLabels.CREATE }
+          },
+          {
+            path: `silence/${URLVerbs.EDIT}/:id`,
+            component: SilenceFormComponent,
+            data: { breadcrumbs: ActionLabels.EDIT }
+          },
+          {
+            path: `silence/${URLVerbs.RECREATE}/:id`,
+            component: SilenceFormComponent,
+            data: { breadcrumbs: ActionLabels.RECREATE }
+          }
+        ]
       },
       {
-        path: 'edit/:name',
-        component: MgrModuleFormComponent,
+        path: 'perf_counters/:type/:id',
+        component: PerformanceCounterComponent,
+        canActivate: [AuthGuardService],
         data: {
-          breadcrumbs: StartCaseBreadcrumbsResolver
+          breadcrumbs: PerformanceCounterBreadcrumbsResolver
         }
-      }
-    ]
-  },
-  // Pools
-  {
-    path: 'pool',
-    canActivate: [AuthGuardService],
-    canActivateChild: [AuthGuardService],
-    data: { breadcrumbs: 'Pools' },
-    loadChildren: () => import('./ceph/pool/pool.module').then((m) => m.RoutedPoolModule)
-  },
-  // Block
-  {
-    path: 'block',
-    canActivateChild: [AuthGuardService],
-    canActivate: [AuthGuardService],
-    data: { breadcrumbs: true, text: 'Block', path: null },
-    loadChildren: () => import('./ceph/block/block.module').then((m) => m.RoutedBlockModule)
-  },
-  // Filesystems
-  {
-    path: 'cephfs',
-    component: CephfsListComponent,
-    canActivate: [FeatureTogglesGuardService, AuthGuardService],
-    data: { breadcrumbs: 'Filesystems' }
-  },
-  // Object Gateway
-  {
-    path: 'rgw',
-    canActivateChild: [FeatureTogglesGuardService, ModuleStatusGuardService, AuthGuardService],
-    data: {
-      moduleStatusGuardConfig: {
-        apiPath: 'rgw',
-        redirectTo: 'rgw/501'
       },
-      breadcrumbs: true,
-      text: 'Object Gateway',
-      path: null
-    },
-    loadChildren: () => import('./ceph/rgw/rgw.module').then((m) => m.RoutedRgwModule)
-  },
-  // User/Role Management
-  {
-    path: 'user-management',
-    canActivate: [AuthGuardService],
-    canActivateChild: [AuthGuardService],
-    data: { breadcrumbs: 'User management', path: null },
-    loadChildren: () => import('./core/auth/auth.module').then((m) => m.RoutedAuthModule)
-  },
-  // User Profile
-  {
-    path: 'user-profile',
-    canActivate: [AuthGuardService],
-    canActivateChild: [AuthGuardService],
-    data: { breadcrumbs: 'User profile', path: null },
-    children: [
+      // Mgr modules
       {
-        path: URLVerbs.EDIT,
-        component: UserPasswordFormComponent,
-        canActivate: [NoSsoGuardService],
-        data: { breadcrumbs: ActionLabels.EDIT }
-      }
-    ]
-  },
-  // NFS
-  {
-    path: 'nfs/501/:message',
-    component: Nfs501Component,
-    canActivate: [AuthGuardService],
-    data: { breadcrumbs: 'NFS' }
-  },
-  {
-    path: 'nfs',
-    canActivate: [AuthGuardService],
-    canActivateChild: [AuthGuardService, ModuleStatusGuardService],
-    data: {
-      moduleStatusGuardConfig: {
-        apiPath: 'nfs-ganesha',
-        redirectTo: 'nfs/501'
+        path: 'mgr-modules',
+        canActivate: [AuthGuardService],
+        canActivateChild: [AuthGuardService],
+        data: { breadcrumbs: 'Cluster/Manager modules' },
+        children: [
+          {
+            path: '',
+            component: MgrModuleListComponent
+          },
+          {
+            path: 'edit/:name',
+            component: MgrModuleFormComponent,
+            data: {
+              breadcrumbs: StartCaseBreadcrumbsResolver
+            }
+          }
+        ]
       },
-      breadcrumbs: 'NFS'
-    },
-    children: [
-      { path: '', component: NfsListComponent },
+      // Pools
+      {
+        path: 'pool',
+        canActivate: [AuthGuardService],
+        canActivateChild: [AuthGuardService],
+        data: { breadcrumbs: 'Pools' },
+        loadChildren: () => import('./ceph/pool/pool.module').then((m) => m.RoutedPoolModule)
+      },
+      // Block
+      {
+        path: 'block',
+        canActivateChild: [AuthGuardService],
+        canActivate: [AuthGuardService],
+        data: { breadcrumbs: true, text: 'Block', path: null },
+        loadChildren: () => import('./ceph/block/block.module').then((m) => m.RoutedBlockModule)
+      },
+      // Filesystems
+      {
+        path: 'cephfs',
+        component: CephfsListComponent,
+        canActivate: [FeatureTogglesGuardService, AuthGuardService],
+        data: { breadcrumbs: 'Filesystems' }
+      },
+      // Object Gateway
+      {
+        path: 'rgw',
+        canActivateChild: [FeatureTogglesGuardService, ModuleStatusGuardService, AuthGuardService],
+        data: {
+          moduleStatusGuardConfig: {
+            apiPath: 'rgw',
+            redirectTo: 'rgw/501'
+          },
+          breadcrumbs: true,
+          text: 'Object Gateway',
+          path: null
+        },
+        loadChildren: () => import('./ceph/rgw/rgw.module').then((m) => m.RoutedRgwModule)
+      },
+      // User/Role Management
       {
-        path: URLVerbs.CREATE,
-        component: NfsFormComponent,
-        data: { breadcrumbs: ActionLabels.CREATE }
+        path: 'user-management',
+        canActivate: [AuthGuardService],
+        canActivateChild: [AuthGuardService],
+        data: { breadcrumbs: 'User management', path: null },
+        loadChildren: () => import('./core/auth/auth.module').then((m) => m.RoutedAuthModule)
       },
+      // User Profile
       {
-        path: `${URLVerbs.EDIT}/:cluster_id/:export_id`,
-        component: NfsFormComponent,
-        data: { breadcrumbs: ActionLabels.EDIT }
+        path: 'user-profile',
+        canActivate: [AuthGuardService],
+        canActivateChild: [AuthGuardService],
+        data: { breadcrumbs: 'User profile', path: null },
+        children: [
+          {
+            path: URLVerbs.EDIT,
+            component: UserPasswordFormComponent,
+            canActivate: [NoSsoGuardService],
+            data: { breadcrumbs: ActionLabels.EDIT }
+          }
+        ]
+      },
+      // NFS
+      {
+        path: 'nfs/501/:message',
+        component: Nfs501Component,
+        canActivate: [AuthGuardService],
+        data: { breadcrumbs: 'NFS' }
+      },
+      {
+        path: 'nfs',
+        canActivate: [AuthGuardService],
+        canActivateChild: [AuthGuardService, ModuleStatusGuardService],
+        data: {
+          moduleStatusGuardConfig: {
+            apiPath: 'nfs-ganesha',
+            redirectTo: 'nfs/501'
+          },
+          breadcrumbs: 'NFS'
+        },
+        children: [
+          { path: '', component: NfsListComponent },
+          {
+            path: URLVerbs.CREATE,
+            component: NfsFormComponent,
+            data: { breadcrumbs: ActionLabels.CREATE }
+          },
+          {
+            path: `${URLVerbs.EDIT}/:cluster_id/:export_id`,
+            component: NfsFormComponent,
+            data: { breadcrumbs: ActionLabels.EDIT }
+          }
+        ]
       }
     ]
   },
-  // Single Sign-On (SSO)
-  { path: 'sso/404', component: SsoNotFoundComponent },
-  // System
-  { path: 'login', component: LoginComponent },
-  { path: 'logout', children: [] },
-  { path: '403', component: ForbiddenComponent },
-  { path: '404', component: NotFoundComponent },
-  { path: '**', redirectTo: '/404' }
+  {
+    path: '',
+    component: BlankLayoutComponent,
+    children: [
+      // Single Sign-On (SSO)
+      { path: 'sso/404', component: SsoNotFoundComponent },
+      // System
+      { path: 'login', component: LoginComponent },
+      { path: 'logout', children: [] },
+      { path: '403', component: ForbiddenComponent },
+      { path: '404', component: NotFoundComponent },
+      { path: '**', redirectTo: '/404' }
+    ]
+  }
 ];
 
 @NgModule({
index 9643a7f3329f99aea48a98d00f99bdb3e773337c..0680b43f9c6ae05df91c576141f20ed411d07c7d 100644 (file)
@@ -1,33 +1 @@
-<!-- Container for sidebar(s) + page content -->
-<ng-sidebar-container>
-
-  <!-- A sidebar -->
-  <ng-sidebar #sidebar
-              [(opened)]="sidebarOpened"
-              [animate]="sidebarAnimate"
-              position="end"
-              mode="over"
-              autoFocus="false"
-              closeOnClickOutside="true"
-              [ngClass]="{'isPwdDisplayed': isPwdDisplayed}">
-    <cd-notifications-sidebar *ngIf="!isLoginActive()"></cd-notifications-sidebar>
-  </ng-sidebar>
-
-  <!-- Page content -->
-  <div ng-sidebar-content>
-    <block-ui>
-      <div *ngIf="isLoginActive()"
-           class="container-fluid full-height">
-        <router-outlet></router-outlet>
-      </div>
-
-      <cd-navigation *ngIf="!isLoginActive()">
-        <div class="container-fluid"
-             [ngClass]="{'dashboard':isDashboardPage()} ">
-          <cd-breadcrumbs></cd-breadcrumbs>
-          <router-outlet></router-outlet>
-        </div>
-      </cd-navigation>
-    </block-ui>
-  </div>
-</ng-sidebar-container>
+<router-outlet></router-outlet>
index 9892d3048f0486c2fa74005e3018707a0a801363..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 (file)
@@ -1,33 +0,0 @@
-@import 'defaults';
-
-.dashboard {
-  background-color: $color-whitesmoke-gray;
-  margin: 0;
-  padding: 0;
-  height: 100%;
-  overflow: overlay;
-}
-
-::ng-deep #toast-container {
-  margin-top: 2vw;
-
-  @media (max-width: 1600px) {
-    margin-top: 2.5vw;
-  }
-
-  @media (max-width: $screen-md-max) {
-    margin-top: 9vw;
-  }
-
-  @media (max-width: 900px) {
-    margin-top: 10vw;
-  }
-
-  @media (max-width: 319px) {
-    margin-top: 11vw;
-  }
-
-  @media (max-width: 260px) {
-    margin-top: 14vw;
-  }
-}
index c3ef68849288855f367ab0506bd4aef8172c04d6..b7a396a5d908a875964a48d91f9cef48a6f1463c 100644 (file)
@@ -1,34 +1,18 @@
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { NO_ERRORS_SCHEMA } from '@angular/core';
-import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
 import { RouterTestingModule } from '@angular/router/testing';
 
-import { SidebarModule } from 'ng-sidebar';
-import { ToastrModule } from 'ngx-toastr';
-
-import { configureTestBed, i18nProviders } from '../testing/unit-test-helper';
 import { AppComponent } from './app.component';
-import { RbdService } from './shared/api/rbd.service';
-import { PipesModule } from './shared/pipes/pipes.module';
-import { AuthStorageService } from './shared/services/auth-storage.service';
-import { NotificationService } from './shared/services/notification.service';
 
 describe('AppComponent', () => {
   let component: AppComponent;
   let fixture: ComponentFixture<AppComponent>;
 
-  configureTestBed({
-    imports: [
-      RouterTestingModule,
-      ToastrModule.forRoot(),
-      PipesModule,
-      HttpClientTestingModule,
-      SidebarModule.forRoot()
-    ],
-    declarations: [AppComponent],
-    schemas: [NO_ERRORS_SCHEMA],
-    providers: [AuthStorageService, i18nProviders, RbdService]
-  });
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [AppComponent],
+      imports: [RouterTestingModule]
+    }).compileComponents();
+  }));
 
   beforeEach(() => {
     fixture = TestBed.createComponent(AppComponent);
@@ -39,38 +23,4 @@ describe('AppComponent', () => {
   it('should create', () => {
     expect(component).toBeTruthy();
   });
-
-  describe('Sidebar', () => {
-    let notificationService: NotificationService;
-
-    beforeEach(() => {
-      notificationService = TestBed.get(NotificationService);
-    });
-
-    it('should always close if sidebarSubject value is true', () => {
-      // Closed before next value
-      expect(component.sidebarOpened).toBeFalsy();
-      notificationService.sidebarSubject.next(true);
-      expect(component.sidebarOpened).toBeFalsy();
-
-      // Opened before next value
-      component.sidebarOpened = true;
-      expect(component.sidebarOpened).toBeTruthy();
-      notificationService.sidebarSubject.next(true);
-      expect(component.sidebarOpened).toBeFalsy();
-    });
-
-    it('should toggle sidebar visibility if sidebarSubject value is false', () => {
-      // Closed before next value
-      expect(component.sidebarOpened).toBeFalsy();
-      notificationService.sidebarSubject.next(false);
-      expect(component.sidebarOpened).toBeTruthy();
-
-      // Opened before next value
-      component.sidebarOpened = true;
-      expect(component.sidebarOpened).toBeTruthy();
-      notificationService.sidebarSubject.next(false);
-      expect(component.sidebarOpened).toBeFalsy();
-    });
-  });
 });
index 216042dea461ea98ee146a1aa18af5701bb8f16f..6960bb3b225b6af4d97c11a4dafe55bf6cebf3de 100644 (file)
@@ -1,63 +1,10 @@
-import { Component, ViewChild } from '@angular/core';
-import { Router } from '@angular/router';
-
-import { Sidebar } from 'ng-sidebar';
-import { TooltipConfig } from 'ngx-bootstrap/tooltip';
-
-import { AuthStorageService } from './shared/services/auth-storage.service';
-import { NotificationService } from './shared/services/notification.service';
+import { Component } from '@angular/core';
 
 @Component({
   selector: 'cd-root',
   templateUrl: './app.component.html',
-  styleUrls: ['./app.component.scss'],
-  providers: [
-    {
-      provide: TooltipConfig,
-      useFactory: (): TooltipConfig =>
-        Object.assign(new TooltipConfig(), {
-          container: 'body'
-        })
-    }
-  ]
+  styleUrls: ['./app.component.scss']
 })
 export class AppComponent {
-  @ViewChild(Sidebar, { static: true })
-  sidebar: Sidebar;
-
-  title = 'cd';
-
-  sidebarOpened = false;
-  // There is a bug in ng-sidebar that will show the sidebar closing animation
-  // when the page is first loaded. This prevents that.
-  sidebarAnimate = false;
-
-  isPwdDisplayed = false;
-
-  constructor(
-    private authStorageService: AuthStorageService,
-    private router: Router,
-    public notificationService: NotificationService
-  ) {
-    this.notificationService.sidebarSubject.subscribe((forcedClose) => {
-      if (forcedClose) {
-        this.sidebar.close();
-      } else {
-        this.sidebarAnimate = true;
-        this.sidebarOpened = !this.sidebarOpened;
-      }
-    });
-
-    this.authStorageService.isPwdDisplayed$.subscribe((isDisplayed) => {
-      this.isPwdDisplayed = isDisplayed;
-    });
-  }
-
-  isLoginActive() {
-    return this.router.url === '/login' || !this.authStorageService.isLoggedIn();
-  }
-
-  isDashboardPage() {
-    return this.router.url === '/dashboard';
-  }
+  constructor() {}
 }
index 3b6e44f3175bad3f730b3c8c4d5b955abbaa2a72..a2bed3306e93c0c7c3533f3b577c3abb63d59576 100644 (file)
@@ -11,9 +11,8 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
 
 import { JwtModule } from '@auth0/angular-jwt';
 import { I18n } from '@ngx-translate/i18n-polyfill';
-import { BlockUIModule } from 'ng-block-ui';
 import { NgBootstrapFormValidationModule } from 'ng-bootstrap-form-validation';
-import { SidebarModule } from 'ng-sidebar';
+
 import { AccordionModule } from 'ngx-bootstrap/accordion';
 import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
 import { TabsModule } from 'ngx-bootstrap/tabs';
@@ -38,7 +37,6 @@ export function jwtTokenGetter() {
   declarations: [AppComponent],
   imports: [
     HttpClientModule,
-    BlockUIModule.forRoot(),
     BrowserModule,
     BrowserAnimationsModule,
     ToastrModule.forRoot({
@@ -59,7 +57,6 @@ export function jwtTokenGetter() {
       }
     }),
     NgBootstrapFormValidationModule.forRoot(),
-    SidebarModule.forRoot(),
     WebStorageModule
   ],
   exports: [SharedModule],
index bb5e01f3388940f4cb8f25aeebbd64d1432a9473..e088fe0b04326ba1616ec30ea967896ddcf8aca9 100644 (file)
@@ -4,7 +4,6 @@
   height: 100vh;
   color: $color-login-row-text;
   background-color: $color-login-row-bg;
-  margin: 0 -30px;
 
   header {
     position: absolute;
index 6c94c2e7841cc7a1e99ceb731ba206dd9cf7a37c..b29ede92f8e0d0a99c2ed6adc85ad51ef90cdc07 100644 (file)
@@ -1,4 +1,4 @@
-<div class="row">
+<div class="vertical-align full-height">
   <div class="col-md-12 text-center">
     <h1 i18n>Sorry, the user does not exist in Ceph.</h1>
     <h4 i18n>Return to <a class="sso-logout"
@@ -6,6 +6,7 @@
 
     <img class="img-fluid mx-auto rounded"
          src="assets/1280px-Nautilus_Octopus.jpg">
+    <br>
     <span>
       "<a href="https://www.flickr.com/photos/146401137@N06/40335060661">Nautilus Octopus</a>" by Jin Kemoole is licensed under
       <a rel="nofollow"
index 8d3d67c652071a1ff55794591d5290ae50b816f8..e6194267320cbc0cdcb904a57bca5a1b094371b7 100644 (file)
@@ -1,13 +1,32 @@
 import { CommonModule } from '@angular/common';
 import { NgModule } from '@angular/core';
+import { RouterModule } from '@angular/router';
 
+import { BlockUIModule } from 'ng-block-ui';
+import { SidebarModule } from 'ng-sidebar';
+
+import { SharedModule } from '../shared/shared.module';
 import { ForbiddenComponent } from './forbidden/forbidden.component';
+import { BlankLayoutComponent } from './layouts/blank-layout/blank-layout.component';
+import { WorkbenchLayoutComponent } from './layouts/workbench-layout/workbench-layout.component';
 import { NavigationModule } from './navigation/navigation.module';
 import { NotFoundComponent } from './not-found/not-found.component';
 
 @NgModule({
-  imports: [CommonModule, NavigationModule],
+  imports: [
+    BlockUIModule.forRoot(),
+    CommonModule,
+    NavigationModule,
+    RouterModule,
+    SharedModule,
+    SidebarModule.forRoot()
+  ],
   exports: [NavigationModule],
-  declarations: [NotFoundComponent, ForbiddenComponent]
+  declarations: [
+    NotFoundComponent,
+    ForbiddenComponent,
+    WorkbenchLayoutComponent,
+    BlankLayoutComponent
+  ]
 })
 export class CoreModule {}
index 81ea8c0aaa429be0211fb2196614803e2de0f11e..5865bd02a1bdc89a8981c17843b056458d821b58 100644 (file)
@@ -1,10 +1,12 @@
-<div class="row">
-  <div class="col-md-12 text-center">
+<div class="horizontal-align vertical-align full-height">
+  <div class="text-center">
     <h1 i18n>Forbidden</h1>
-
     <i class="{{ icons.lock }} text-danger"></i>
-
     <h2 i18n>Sorry, you are not allowed to see what you were looking for.</h2>
-
+    <button type="button"
+            class="btn btn-primary"
+            [routerLink]="'/login'">
+      <ng-container i18n>Back</ng-container>
+    </button>
   </div>
 </div>
index 3ed7d32d21d01e94020f12d15a468a9dd1bba7b1..7a53554d7535e1d6a3d8eb3ce42da0ffdbb8308d 100644 (file)
@@ -1,4 +1,5 @@
 import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
 
 import { configureTestBed } from '../../../testing/unit-test-helper';
 import { ForbiddenComponent } from './forbidden.component';
@@ -8,7 +9,8 @@ describe('ForbiddenComponent', () => {
   let fixture: ComponentFixture<ForbiddenComponent>;
 
   configureTestBed({
-    declarations: [ForbiddenComponent]
+    declarations: [ForbiddenComponent],
+    imports: [RouterTestingModule]
   });
 
   beforeEach(() => {
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.html
new file mode 100644 (file)
index 0000000..0680b43
--- /dev/null
@@ -0,0 +1 @@
+<router-outlet></router-outlet>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.scss
new file mode 100644 (file)
index 0000000..0de9b5f
--- /dev/null
@@ -0,0 +1,25 @@
+@import 'defaults';
+
+::ng-deep #toast-container {
+  margin-top: 2vw;
+
+  @media (max-width: 1600px) {
+    margin-top: 2.5vw;
+  }
+
+  @media (max-width: $screen-md-max) {
+    margin-top: 9vw;
+  }
+
+  @media (max-width: 900px) {
+    margin-top: 10vw;
+  }
+
+  @media (max-width: 319px) {
+    margin-top: 11vw;
+  }
+
+  @media (max-width: 260px) {
+    margin-top: 14vw;
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.spec.ts
new file mode 100644 (file)
index 0000000..75c3686
--- /dev/null
@@ -0,0 +1,26 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { BlankLayoutComponent } from './blank-layout.component';
+
+describe('DefaultLayoutComponent', () => {
+  let component: BlankLayoutComponent;
+  let fixture: ComponentFixture<BlankLayoutComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [BlankLayoutComponent],
+      imports: [RouterTestingModule]
+    }).compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(BlankLayoutComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.ts
new file mode 100644 (file)
index 0000000..a0fef8e
--- /dev/null
@@ -0,0 +1,10 @@
+import { Component } from '@angular/core';
+
+@Component({
+  selector: 'cd-blank-layout',
+  templateUrl: './blank-layout.component.html',
+  styleUrls: ['./blank-layout.component.scss']
+})
+export class BlankLayoutComponent {
+  constructor() {}
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html
new file mode 100644 (file)
index 0000000..1b32a4d
--- /dev/null
@@ -0,0 +1,26 @@
+<!-- Container for sidebar(s) + page content -->
+<ng-sidebar-container>
+  <!-- A sidebar -->
+  <ng-sidebar #sidebar
+              [(opened)]="sidebarOpened"
+              [animate]="sidebarAnimate"
+              position="end"
+              mode="over"
+              autoFocus="false"
+              closeOnClickOutside="true">
+    <cd-notifications-sidebar></cd-notifications-sidebar>
+  </ng-sidebar>
+
+  <!-- Page content -->
+  <div ng-sidebar-content>
+    <block-ui>
+      <cd-navigation>
+        <div class="container-fluid"
+             [ngClass]="{'dashboard':isDashboardPage()} ">
+          <cd-breadcrumbs></cd-breadcrumbs>
+          <router-outlet></router-outlet>
+        </div>
+      </cd-navigation>
+    </block-ui>
+  </div>
+</ng-sidebar-container>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.scss
new file mode 100644 (file)
index 0000000..238524b
--- /dev/null
@@ -0,0 +1,31 @@
+@import 'defaults';
+
+.dashboard {
+  background-color: $color-whitesmoke-gray;
+  margin: 0;
+  padding: 0;
+}
+
+::ng-deep #toast-container {
+  margin-top: 2vw;
+
+  @media (max-width: 1600px) {
+    margin-top: 2.5vw;
+  }
+
+  @media (max-width: $screen-md-max) {
+    margin-top: 9vw;
+  }
+
+  @media (max-width: 900px) {
+    margin-top: 10vw;
+  }
+
+  @media (max-width: 319px) {
+    margin-top: 11vw;
+  }
+
+  @media (max-width: 260px) {
+    margin-top: 14vw;
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.spec.ts
new file mode 100644 (file)
index 0000000..4bf4a04
--- /dev/null
@@ -0,0 +1,76 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { SidebarModule } from 'ng-sidebar';
+import { ToastrModule } from 'ngx-toastr';
+
+import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper';
+import { RbdService } from '../../../shared/api/rbd.service';
+import { PipesModule } from '../../../shared/pipes/pipes.module';
+import { AuthStorageService } from '../../../shared/services/auth-storage.service';
+import { NotificationService } from '../../../shared/services/notification.service';
+import { WorkbenchLayoutComponent } from './workbench-layout.component';
+
+describe('WorkbenchLayoutComponent', () => {
+  let component: WorkbenchLayoutComponent;
+  let fixture: ComponentFixture<WorkbenchLayoutComponent>;
+
+  configureTestBed({
+    imports: [
+      RouterTestingModule,
+      ToastrModule.forRoot(),
+      PipesModule,
+      HttpClientTestingModule,
+      SidebarModule.forRoot()
+    ],
+    declarations: [WorkbenchLayoutComponent],
+    schemas: [NO_ERRORS_SCHEMA],
+    providers: [AuthStorageService, i18nProviders, RbdService]
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(WorkbenchLayoutComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  describe('Sidebar', () => {
+    let notificationService: NotificationService;
+
+    beforeEach(() => {
+      notificationService = TestBed.get(NotificationService);
+    });
+
+    it('should always close if sidebarSubject value is true', () => {
+      // Closed before next value
+      expect(component.sidebarOpened).toBeFalsy();
+      notificationService.sidebarSubject.next(true);
+      expect(component.sidebarOpened).toBeFalsy();
+
+      // Opened before next value
+      component.sidebarOpened = true;
+      expect(component.sidebarOpened).toBeTruthy();
+      notificationService.sidebarSubject.next(true);
+      expect(component.sidebarOpened).toBeFalsy();
+    });
+
+    it('should toggle sidebar visibility if sidebarSubject value is false', () => {
+      // Closed before next value
+      expect(component.sidebarOpened).toBeFalsy();
+      notificationService.sidebarSubject.next(false);
+      expect(component.sidebarOpened).toBeTruthy();
+
+      // Opened before next value
+      component.sidebarOpened = true;
+      expect(component.sidebarOpened).toBeTruthy();
+      notificationService.sidebarSubject.next(false);
+      expect(component.sidebarOpened).toBeFalsy();
+    });
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.ts
new file mode 100644 (file)
index 0000000..c913918
--- /dev/null
@@ -0,0 +1,55 @@
+import { Component, OnDestroy, ViewChild } from '@angular/core';
+import { Router } from '@angular/router';
+
+import { Sidebar } from 'ng-sidebar';
+import { TooltipConfig } from 'ngx-bootstrap/tooltip';
+import { Subscription } from 'rxjs';
+
+import { NotificationService } from '../../../shared/services/notification.service';
+
+@Component({
+  selector: 'cd-workbench-layout',
+  templateUrl: './workbench-layout.component.html',
+  styleUrls: ['./workbench-layout.component.scss'],
+  providers: [
+    {
+      provide: TooltipConfig,
+      useFactory: (): TooltipConfig =>
+        Object.assign(new TooltipConfig(), {
+          container: 'body'
+        })
+    }
+  ]
+})
+export class WorkbenchLayoutComponent implements OnDestroy {
+  @ViewChild(Sidebar, { static: true })
+  sidebar: Sidebar;
+
+  sidebarOpened = false;
+  // There is a bug in ng-sidebar that will show the sidebar closing animation
+  // when the page is first loaded. This prevents that.
+  sidebarAnimate = false;
+
+  private readonly sidebarSubscription: Subscription;
+
+  constructor(private router: Router, public notificationService: NotificationService) {
+    this.sidebarSubscription = this.notificationService.sidebarSubject.subscribe((forcedClose) => {
+      if (forcedClose) {
+        this.sidebar.close();
+      } else {
+        this.sidebarAnimate = true;
+        this.sidebarOpened = !this.sidebarOpened;
+      }
+    });
+  }
+
+  ngOnDestroy() {
+    if (this.sidebarSubscription) {
+      this.sidebarSubscription.unsubscribe();
+    }
+  }
+
+  isDashboardPage() {
+    return this.router.url === '/dashboard';
+  }
+}
index 8c8f6113ae5bd35522143ac111b496b5eb45ebda..734095b5a92417b6118e2d3d932848b3f7f84103 100644 (file)
@@ -1,14 +1,20 @@
-<div class="row">
-  <div class="col-md-12 text-center">
-    <h1 i18n>Sorry, we could not find what you were looking for</h1>
-
+<div class="horizontal-align vertical-align full-height">
+  <div class="text-center">
+    <h1 i18n>Sorry, we could not find what you were looking for.</h1>
     <img class="img-fluid mx-auto rounded"
          src="assets/1280px-Nautilus_Octopus.jpg">
+    <br>
     <span>
       "<a href="https://www.flickr.com/photos/146401137@N06/40335060661">Nautilus Octopus</a>" by Jin Kemoole is licensed under
       <a rel="nofollow"
          class="external text"
          href="https://creativecommons.org/licenses/by/2.0/">CC BY 2.0</a>
     </span>
+    <br>
+    <button type="button"
+            class="btn btn-primary"
+            [routerLink]="'/login'">
+      <ng-container i18n>Back</ng-container>
+    </button>
   </div>
 </div>
index fdf2e7100f6158eec9edf6d64135d1dfa383730a..a14ea5da437855f5bb2542d5d4699d638c146448 100644 (file)
@@ -1,5 +1,6 @@
 h1 {
   font-size: -webkit-xxx-large;
+  font-family: monospace;
 }
 
 * {
index b75232c204c22998eda3df2bfd3cdccfbedb689e..3adb9c3f32f36b46277541614bcf586c96fe99cb 100644 (file)
@@ -1,4 +1,5 @@
 import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
 
 import { configureTestBed } from '../../../testing/unit-test-helper';
 import { NotFoundComponent } from './not-found.component';
@@ -8,7 +9,8 @@ describe('NotFoundComponent', () => {
   let fixture: ComponentFixture<NotFoundComponent>;
 
   configureTestBed({
-    declarations: [NotFoundComponent]
+    declarations: [NotFoundComponent],
+    imports: [RouterTestingModule]
   });
 
   beforeEach(() => {
index 7e19d62af8168da9b1c008089a299a093389bce5..226081747d56018d1118c5197dcf1821f82f4523 100644 (file)
@@ -87,6 +87,10 @@ option {
   display: flex;
   align-items: center;
 }
+.horizontal-align {
+  display: flex;
+  justify-content: center;
+}
 .loading {
   position: absolute;
   top: 50%;