From c320371e7d053efbbabc7ef3efbbe1152c2e0ed3 Mon Sep 17 00:00:00 2001 From: Volker Theile Date: Mon, 13 Jan 2020 10:06:25 +0100 Subject: [PATCH] mgr/dashboard: Introduce layout components 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 --- .../frontend/src/app/app-routing.module.ts | 420 +++++++++--------- .../frontend/src/app/app.component.html | 34 +- .../frontend/src/app/app.component.scss | 33 -- .../frontend/src/app/app.component.spec.ts | 64 +-- .../frontend/src/app/app.component.ts | 59 +-- .../dashboard/frontend/src/app/app.module.ts | 5 +- .../app/core/auth/login/login.component.scss | 1 - .../sso-not-found.component.html | 3 +- .../frontend/src/app/core/core.module.ts | 23 +- .../core/forbidden/forbidden.component.html | 12 +- .../forbidden/forbidden.component.spec.ts | 4 +- .../blank-layout/blank-layout.component.html | 1 + .../blank-layout/blank-layout.component.scss | 25 ++ .../blank-layout.component.spec.ts | 26 ++ .../blank-layout/blank-layout.component.ts | 10 + .../workbench-layout.component.html | 26 ++ .../workbench-layout.component.scss | 31 ++ .../workbench-layout.component.spec.ts | 76 ++++ .../workbench-layout.component.ts | 55 +++ .../core/not-found/not-found.component.html | 14 +- .../core/not-found/not-found.component.scss | 1 + .../not-found/not-found.component.spec.ts | 4 +- .../mgr/dashboard/frontend/src/styles.scss | 4 + 23 files changed, 530 insertions(+), 401 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.ts diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts index 2913b293dc3..fe7be74abe7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts @@ -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({ diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app.component.html b/src/pybind/mgr/dashboard/frontend/src/app/app.component.html index 9643a7f3329..0680b43f9c6 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/app.component.html @@ -1,33 +1 @@ - - - - - - - - - -
- -
- -
- - -
- - -
-
-
-
-
+ diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/app.component.scss index 9892d3048f0..e69de29bb2d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/app.component.scss @@ -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; - } -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/app.component.spec.ts index c3ef6884928..b7a396a5d90 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/app.component.spec.ts @@ -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; - 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(); - }); - }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/app.component.ts index 216042dea46..6960bb3b225 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/app.component.ts @@ -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() {} } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/app.module.ts index 3b6e44f3175..a2bed3306e9 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/app.module.ts @@ -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], diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.scss index bb5e01f3388..e088fe0b043 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.scss @@ -4,7 +4,6 @@ height: 100vh; color: $color-login-row-text; background-color: $color-login-row-bg; - margin: 0 -30px; header { position: absolute; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/sso/sso-not-found/sso-not-found.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/sso/sso-not-found/sso-not-found.component.html index 6c94c2e7841..b29ede92f8e 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/sso/sso-not-found/sso-not-found.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/sso/sso-not-found/sso-not-found.component.html @@ -1,4 +1,4 @@ -
+

Sorry, the user does not exist in Ceph.

Return to +
"
Nautilus Octopus" by Jin Kemoole is licensed under -
+
+

Forbidden

- -

Sorry, you are not allowed to see what you were looking for.

- +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/forbidden/forbidden.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/forbidden/forbidden.component.spec.ts index 3ed7d32d21d..7a53554d753 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/forbidden/forbidden.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/forbidden/forbidden.component.spec.ts @@ -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; 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 index 00000000000..0680b43f9c6 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.html @@ -0,0 +1 @@ + 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 index 00000000000..0de9b5f8c09 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.scss @@ -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 index 00000000000..75c3686d488 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.spec.ts @@ -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; + + 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 index 00000000000..a0fef8e4a41 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/blank-layout/blank-layout.component.ts @@ -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 index 00000000000..1b32a4dfea5 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html @@ -0,0 +1,26 @@ + + + + + + + + +
+ + +
+ + +
+
+
+
+
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 index 00000000000..238524b866d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.scss @@ -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 index 00000000000..4bf4a049418 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.spec.ts @@ -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; + + 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 index 00000000000..c913918fd34 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.ts @@ -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'; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.html index 8c8f6113ae5..734095b5a92 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.html @@ -1,14 +1,20 @@ -
-
-

Sorry, we could not find what you were looking for

- +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.scss index fdf2e7100f6..a14ea5da437 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.scss @@ -1,5 +1,6 @@ h1 { font-size: -webkit-xxx-large; + font-family: monospace; } * { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.spec.ts index b75232c204c..3adb9c3f32f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.spec.ts @@ -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; configureTestBed({ - declarations: [NotFoundComponent] + declarations: [NotFoundComponent], + imports: [RouterTestingModule] }); beforeEach(() => { diff --git a/src/pybind/mgr/dashboard/frontend/src/styles.scss b/src/pybind/mgr/dashboard/frontend/src/styles.scss index 7e19d62af81..226081747d5 100644 --- a/src/pybind/mgr/dashboard/frontend/src/styles.scss +++ b/src/pybind/mgr/dashboard/frontend/src/styles.scss @@ -87,6 +87,10 @@ option { display: flex; align-items: center; } +.horizontal-align { + display: flex; + justify-content: center; +} .loading { position: absolute; top: 50%; -- 2.39.5