]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: new generic HTTP error page component
authorAashish Sharma <aashishsharma@localhost.localdomain>
Tue, 27 Oct 2020 07:06:24 +0000 (12:36 +0530)
committerAashish Sharma <aashishsharma@localhost.localdomain>
Fri, 11 Dec 2020 13:49:38 +0000 (19:19 +0530)
Added a generic Error component for HTTP Errors such as 404,403,501

Fixes:https://tracker.ceph.com/issues/39128
Signed-off-by: Aashish Sharma <aasharma@redhat.com>
53 files changed:
doc/dev/developer_guide/dash-devel.rst
src/pybind/mgr/dashboard/frontend/cypress/integration/nfs/nfs.e2e-spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/cypress/integration/nfs/nfs.po.ts [deleted file]
src/pybind/mgr/dashboard/frontend/cypress/integration/page-helper.po.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/ui/navigation.po.ts
src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-501/nfs-501.component.html [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-501/nfs-501.component.scss [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-501/nfs-501.component.spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-501/nfs-501.component.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-501/rgw-501.component.html [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-501/rgw-501.component.scss [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-501/rgw-501.component.spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-501/rgw-501.component.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/auth.module.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/sso/sso-not-found/sso-not-found.component.html [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/sso/sso-not-found/sso-not-found.component.scss [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/sso/sso-not-found/sso-not-found.component.spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/core/auth/sso/sso-not-found/sso-not-found.component.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/core/core.module.ts
src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/error/error.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/forbidden/forbidden.component.html [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/core/forbidden/forbidden.component.scss [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/core/forbidden/forbidden.component.spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/core/forbidden/forbidden.component.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.scss
src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.html [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.scss [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/api-interceptor.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/api-interceptor.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/feature-toggles-guard.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/feature-toggles-guard.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/js-error-handler.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/module-status-guard.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/module-status-guard.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/no-sso-guard.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/no-sso-guard.service.ts
src/pybind/mgr/dashboard/frontend/src/assets/1500px-Southern_Keeled_Octopus.jpg [deleted file]
src/pybind/mgr/dashboard/frontend/src/styles/defaults/_bootstrap-defaults.scss
src/pybind/mgr/dashboard/services/orchestrator.py

index 5401cf9d13313618b157f44c0d2d328353786834..130a6b9a65d40e74368d90f1193222e918a246ed 100644 (file)
@@ -10,7 +10,7 @@ Feature Design
 
 To promote collaboration on new Ceph Dashboard features, the first step is
 the definition of a design document. These documents then form the basis of
-implementation scope and permit wider participation in the evolution of the 
+implementation scope and permit wider participation in the evolution of the
 Ceph Dashboard UI.
 
 .. toctree::
@@ -860,7 +860,7 @@ Buttons are used for performing actions such as: “Submit”, “Edit, “Creat
 button should use the `cd-submit-button` component and the secondary button should
 use `cd-back-button` component. The text on the action button should be same as the
 form title and follow a title case. The text on the secondary button should be
-`Cancel`. `Perform action` button should always be on right while `Cancel` 
+`Cancel`. `Perform action` button should always be on right while `Cancel`
 button should always be on left.
 
 **Modals**: The main action button should use the `cd-submit-button` component and
@@ -878,7 +878,7 @@ same as the form's main button color.
 
 **Drop Down Buttons:** Use dropdown buttons to display predefined lists of
 actions. All drop down buttons have icons corresponding to the action they
-perform.  
+perform.
 
 Links
 .....
@@ -926,6 +926,19 @@ Alerts and notifications
 Default notification should have `text-info` color. Success notification should
 have `text-success` color. Failure notification should have `text-danger` color.
 
+Error Handling
+~~~~~~~~~~~~~~
+
+For handling front-end errors, there is a generic Error Component which can be
+found in ``./src/pybind/mgr/dashboard/frontend/src/app/core/error``. For
+reporting a new error, you can simply extend the ``DashboardError`` class
+in ``error.ts`` file and add specific header and message for the new error. Some
+generic error classes are already in place such as ``DashboardNotFoundError``
+and ``DashboardForbiddenError`` which can be called and reused in different
+scenarios.
+
+For example - ``throw new DashboardNotFoundError()``.
+
 I18N
 ----
 
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/nfs/nfs.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/nfs/nfs.e2e-spec.ts
deleted file mode 100644 (file)
index 0864a5e..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-import { NfsPageHelper } from './nfs.po';
-
-describe('Nfs page', () => {
-  const nfs = new NfsPageHelper();
-
-  beforeEach(() => {
-    cy.login();
-    nfs.navigateTo();
-  });
-
-  describe('breadcrumb test', () => {
-    it('should open and show breadcrumb', () => {
-      nfs.expectBreadcrumbText('NFS');
-    });
-  });
-});
diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/nfs/nfs.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/nfs/nfs.po.ts
deleted file mode 100644 (file)
index 7dd482a..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-import { PageHelper } from '../page-helper.po';
-
-export class NfsPageHelper extends PageHelper {
-  pages = { index: { url: '#/nfs', id: 'cd-nfs-501' } };
-}
index 38edfc451604e5b667af1f32c7a7f1d78425b485..7b099fd98acf21fd73daa9f7c285f4b8a12407b1 100644 (file)
@@ -227,7 +227,7 @@ export abstract class PageHelper {
   }
 
   setPageSize(size: string) {
-    cy.get('cd-table .dataTables_paginate input').first().clear().type(size);
+    cy.get('cd-table .dataTables_paginate input').first().clear({ force: true }).type(size);
   }
 
   seachTable(text: string) {
index a26a5ff10c4c23027b4941fb8b5aa9466570490f..486d30dd646839292e50a9775a269c686be2cca8 100644 (file)
@@ -6,7 +6,7 @@ export class NavigationPageHelper extends PageHelper {
   };
 
   navigations = [
-    { menu: 'NFS', component: 'cd-nfs-501' },
+    { menu: 'NFS', component: 'cd-error' },
     {
       menu: 'Object Gateway',
       submenus: [
@@ -20,9 +20,9 @@ export class NavigationPageHelper extends PageHelper {
       menu: 'Cluster',
       submenus: [
         { menu: 'Hosts', component: 'cd-hosts' },
-        { menu: 'Inventory', component: 'cd-inventory' },
+        { menu: 'Inventory', component: 'cd-error' },
         { menu: 'Monitors', component: 'cd-monitor' },
-        { menu: 'Services', component: 'cd-services' },
+        { menu: 'Services', component: 'cd-error' },
         { menu: 'OSDs', component: 'cd-osd-list' },
         { menu: 'Configuration', component: 'cd-configuration' },
         { menu: 'CRUSH map', component: 'cd-crushmap' },
index f9614c9c577585c8f6d0d441cc434ec2999d9f52..4ca71f17259d23ca4c9379842568071a95a2c2d0 100644 (file)
@@ -24,19 +24,16 @@ import { ServiceFormComponent } from './ceph/cluster/services/service-form/servi
 import { ServicesComponent } from './ceph/cluster/services/services.component';
 import { TelemetryComponent } from './ceph/cluster/telemetry/telemetry.component';
 import { DashboardComponent } from './ceph/dashboard/dashboard/dashboard.component';
-import { Nfs501Component } from './ceph/nfs/nfs-501/nfs-501.component';
 import { NfsFormComponent } from './ceph/nfs/nfs-form/nfs-form.component';
 import { NfsListComponent } from './ceph/nfs/nfs-list/nfs-list.component';
 import { PerformanceCounterComponent } from './ceph/performance-counter/performance-counter/performance-counter.component';
 import { LoginPasswordFormComponent } from './core/auth/login-password-form/login-password-form.component';
 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 { ErrorComponent } from './core/error/error.component';
 import { BlankLayoutComponent } from './core/layouts/blank-layout/blank-layout.component';
 import { LoginLayoutComponent } from './core/layouts/login-layout/login-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';
 import { AuthGuardService } from './shared/services/auth-guard.service';
@@ -87,6 +84,8 @@ const routes: Routes = [
     canActivateChild: [AuthGuardService, ChangePasswordGuardService],
     children: [
       { path: 'dashboard', component: DashboardComponent },
+      { path: 'error', component: ErrorComponent },
+
       // Cluster
       {
         path: 'hosts',
@@ -107,7 +106,17 @@ const routes: Routes = [
       },
       {
         path: 'services',
-        data: { breadcrumbs: 'Cluster/Services' },
+        canActivateChild: [ModuleStatusGuardService],
+        data: {
+          moduleStatusGuardConfig: {
+            apiPath: 'orchestrator',
+            redirectTo: 'error',
+            section: 'orch',
+            section_info: 'Orchestrator',
+            header: 'Orchestrator is not available'
+          },
+          breadcrumbs: 'Cluster/Services'
+        },
         children: [
           { path: '', component: ServicesComponent },
           {
@@ -119,8 +128,18 @@ const routes: Routes = [
       },
       {
         path: 'inventory',
+        canActivate: [ModuleStatusGuardService],
         component: InventoryComponent,
-        data: { breadcrumbs: 'Cluster/Inventory' }
+        data: {
+          moduleStatusGuardConfig: {
+            apiPath: 'orchestrator',
+            redirectTo: 'error',
+            section: 'orch',
+            section_info: 'Orchestrator',
+            header: 'Orchestrator is not available'
+          },
+          breadcrumbs: 'Cluster/Inventory'
+        }
       },
       {
         path: 'osd',
@@ -259,7 +278,10 @@ const routes: Routes = [
         data: {
           moduleStatusGuardConfig: {
             apiPath: 'rgw',
-            redirectTo: 'rgw/501'
+            redirectTo: 'error',
+            section: 'rgw',
+            section_info: 'Object Gateway',
+            header: 'The Object Gateway Service is not configured'
           },
           breadcrumbs: true,
           text: 'Object Gateway',
@@ -287,18 +309,16 @@ const routes: Routes = [
         ]
       },
       // NFS
-      {
-        path: 'nfs/501/:message',
-        component: Nfs501Component,
-        data: { breadcrumbs: 'NFS' }
-      },
       {
         path: 'nfs',
         canActivateChild: [FeatureTogglesGuardService, ModuleStatusGuardService],
         data: {
           moduleStatusGuardConfig: {
             apiPath: 'nfs-ganesha',
-            redirectTo: 'nfs/501'
+            redirectTo: 'error',
+            section: 'nfs-ganesha',
+            section_info: 'NFS GANESHA',
+            header: 'NFS-Ganesha is not configured'
           },
           breadcrumbs: 'NFS'
         },
@@ -333,14 +353,7 @@ const routes: Routes = [
   {
     path: '',
     component: BlankLayoutComponent,
-    children: [
-      // Single Sign-On (SSO)
-      { path: 'sso/404', component: SsoNotFoundComponent },
-      // System
-      { path: '403', component: ForbiddenComponent },
-      { path: '404', component: NotFoundComponent },
-      { path: '**', redirectTo: '/404' }
-    ]
+    children: [{ path: '**', redirectTo: '/error' }]
   }
 ];
 
index 40f6937c66b92f250a280c2dc6921e5ded51a1f2..136375cbbbe38784b793ec7c9f75a1c0bffed944 100644 (file)
@@ -10,7 +10,8 @@ import moment from 'moment';
 import { ToastrModule } from 'ngx-toastr';
 import { of, throwError } from 'rxjs';
 
-import { NotFoundComponent } from '~/app/core/not-found/not-found.component';
+import { DashboardNotFoundError } from '~/app/core/error/error';
+import { ErrorComponent } from '~/app/core/error/error.component';
 import { PrometheusService } from '~/app/shared/api/prometheus.service';
 import { NotificationType } from '~/app/shared/enum/notification-type.enum';
 import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
@@ -49,10 +50,11 @@ describe('SilenceFormComponent', () => {
   // Date mocking related
   const baseTime = '2022-02-22 00:00';
   const beginningDate = '2022-02-22T00:00:12.35';
+  let prometheusPermissions: Permission;
 
-  const routes: Routes = [{ path: '404', component: NotFoundComponent }];
+  const routes: Routes = [{ path: '404', component: ErrorComponent }];
   configureTestBed({
-    declarations: [NotFoundComponent, SilenceFormComponent],
+    declarations: [ErrorComponent, SilenceFormComponent],
     imports: [
       HttpClientTestingModule,
       RouterTestingModule.withRoutes(routes),
@@ -128,6 +130,10 @@ describe('SilenceFormComponent', () => {
     authStorageService = TestBed.inject(AuthStorageService);
     spyOn(authStorageService, 'getUsername').and.returnValue('someUser');
 
+    spyOn(authStorageService, 'getPermissions').and.callFake(() => ({
+      prometheus: prometheusPermissions
+    }));
+    prometheusPermissions = new Permission(['update', 'delete', 'read', 'create']);
     fixture = TestBed.createComponent(SilenceFormComponent);
     fixtureH = new FixtureHelper(fixture);
     component = fixture.componentInstance;
@@ -166,56 +172,53 @@ describe('SilenceFormComponent', () => {
     );
   });
 
-  describe('redirect not allowed users', () => {
-    let prometheusPermissions: Permission;
+  describe('throw error for not allowed users', () => {
     let navigateSpy: jasmine.Spy;
 
-    const expectRedirect = (action: string, redirected: boolean) => {
-      changeAction(action);
-      expect(router.navigate).toHaveBeenCalledTimes(redirected ? 1 : 0);
+    const expectError = (action: string, redirected: boolean) => {
+      Object.defineProperty(router, 'url', { value: action });
       if (redirected) {
-        expect(router.navigate).toHaveBeenCalledWith(['/404']);
+        expect(() => callInit()).toThrowError(DashboardNotFoundError);
+      } else {
+        expect(() => callInit()).not.toThrowError();
       }
       navigateSpy.calls.reset();
     };
 
     beforeEach(() => {
       navigateSpy = spyOn(router, 'navigate').and.stub();
-      spyOn(authStorageService, 'getPermissions').and.callFake(() => ({
-        prometheus: prometheusPermissions
-      }));
     });
 
-    it('redirects to 404 if not allowed', () => {
+    it('should throw error if not allowed', () => {
       prometheusPermissions = new Permission(['delete', 'read']);
-      expectRedirect('add', true);
-      expectRedirect('alertAdd', true);
+      expectError('add', true);
+      expectError('alertAdd', true);
     });
 
-    it('redirects if user does not have minimum permissions to create silences', () => {
+    it('should throw error if user does not have minimum permissions to create silences', () => {
       prometheusPermissions = new Permission(['update', 'delete', 'read']);
-      expectRedirect('add', true);
+      expectError('add', true);
       prometheusPermissions = new Permission(['update', 'delete', 'create']);
-      expectRedirect('recreate', true);
+      expectError('recreate', true);
     });
 
-    it('redirects if user does not have minimum permissions to update silences', () => {
-      prometheusPermissions = new Permission(['create', 'delete', 'read']);
-      expectRedirect('edit', true);
+    it('should throw error if user does not have minimum permissions to update silences', () => {
+      prometheusPermissions = new Permission(['delete', 'read']);
+      expectError('edit', true);
       prometheusPermissions = new Permission(['create', 'delete', 'update']);
-      expectRedirect('edit', true);
+      expectError('edit', true);
     });
 
-    it('does not redirect if user has minimum permissions to create silences', () => {
+    it('does not throw error if user has minimum permissions to create silences', () => {
       prometheusPermissions = new Permission(['create', 'read']);
-      expectRedirect('add', false);
-      expectRedirect('alertAdd', false);
-      expectRedirect('recreate', false);
+      expectError('add', false);
+      expectError('alertAdd', false);
+      expectError('recreate', false);
     });
 
-    it('does not redirect if user has minimum permissions to update silences', () => {
-      prometheusPermissions = new Permission(['update', 'read']);
-      expectRedirect('edit', false);
+    it('does not throw error if user has minimum permissions to update silences', () => {
+      prometheusPermissions = new Permission(['read', 'create']);
+      expectError('edit', false);
     });
   });
 
index 3ffa5caf7d64506667bd1734c2447cb915d8c5bc..b3bc1401082a66487b29197b583f35551cb29b7c 100644 (file)
@@ -5,6 +5,7 @@ import { ActivatedRoute, Router } from '@angular/router';
 import _ from 'lodash';
 import moment from 'moment';
 
+import { DashboardNotFoundError } from '~/app/core/error/error';
 import { PrometheusService } from '~/app/shared/api/prometheus.service';
 import { ActionLabelsI18n, SucceededActionLabelsI18n } from '~/app/shared/constants/app.constants';
 import { Icons } from '~/app/shared/enum/icons.enum';
@@ -107,7 +108,7 @@ export class SilenceFormComponent {
     const allowed =
       this.permission.read && (this.edit ? this.permission.update : this.permission.create);
     if (!allowed) {
-      this.router.navigate(['/404']);
+      throw new DashboardNotFoundError();
     }
   }
 
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-501/nfs-501.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-501/nfs-501.component.html
deleted file mode 100644 (file)
index bcbb881..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-<cd-alert-panel type="info">
-  {{ message }}<br>
-  <ng-container i18n>Please consult the <cd-doc section="nfs-ganesha"></cd-doc> on how
-    to configure and enable the NFS Ganesha management functionality.</ng-container>
-</cd-alert-panel>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-501/nfs-501.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-501/nfs-501.component.scss
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-501/nfs-501.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-501/nfs-501.component.spec.ts
deleted file mode 100644 (file)
index e23b5f7..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { RouterTestingModule } from '@angular/router/testing';
-
-import { SharedModule } from '~/app/shared/shared.module';
-import { configureTestBed } from '~/testing/unit-test-helper';
-import { Nfs501Component } from './nfs-501.component';
-
-describe('Nfs501Component', () => {
-  let component: Nfs501Component;
-  let fixture: ComponentFixture<Nfs501Component>;
-
-  configureTestBed({
-    declarations: [Nfs501Component],
-    imports: [HttpClientTestingModule, RouterTestingModule, SharedModule]
-  });
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(Nfs501Component);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-501/nfs-501.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-501/nfs-501.component.ts
deleted file mode 100644 (file)
index 7f91722..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-import { Component, OnDestroy, OnInit } from '@angular/core';
-import { ActivatedRoute } from '@angular/router';
-
-@Component({
-  selector: 'cd-nfs-501',
-  templateUrl: './nfs-501.component.html',
-  styleUrls: ['./nfs-501.component.scss']
-})
-export class Nfs501Component implements OnInit, OnDestroy {
-  message = $localize`The NFS Ganesha service is not configured.`;
-  routeParamsSubscribe: any;
-
-  constructor(private route: ActivatedRoute) {}
-
-  ngOnInit() {
-    this.routeParamsSubscribe = this.route.params.subscribe((params: { message: string }) => {
-      this.message = params.message;
-    });
-  }
-
-  ngOnDestroy() {
-    this.routeParamsSubscribe.unsubscribe();
-  }
-}
index b19b742cea190b11cf7c441a0fc0ae01013dfa46..4205eb63b26e40f91f68b729a4a181eff9d43124 100644 (file)
@@ -6,7 +6,6 @@ import { RouterModule } from '@angular/router';
 import { NgbNavModule, NgbTooltipModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
 
 import { SharedModule } from '~/app/shared/shared.module';
-import { Nfs501Component } from './nfs-501/nfs-501.component';
 import { NfsDetailsComponent } from './nfs-details/nfs-details.component';
 import { NfsFormClientComponent } from './nfs-form-client/nfs-form-client.component';
 import { NfsFormComponent } from './nfs-form/nfs-form.component';
@@ -22,13 +21,6 @@ import { NfsListComponent } from './nfs-list/nfs-list.component';
     NgbTypeaheadModule,
     NgbTooltipModule
   ],
-  declarations: [
-    NfsListComponent,
-    NfsDetailsComponent,
-    NfsFormComponent,
-    NfsFormClientComponent,
-    Nfs501Component
-  ],
-  exports: [Nfs501Component]
+  declarations: [NfsListComponent, NfsDetailsComponent, NfsFormComponent, NfsFormClientComponent]
 })
 export class NfsModule {}
index 8f63446d5d4949192e0851c1d9eb220ef74c796a..1d58a1778cec7b86331fb92a8ab4b3641db748f4 100644 (file)
@@ -16,7 +16,8 @@ import _ from 'lodash';
 import { ToastrModule } from 'ngx-toastr';
 import { of } from 'rxjs';
 
-import { NotFoundComponent } from '~/app/core/not-found/not-found.component';
+import { DashboardNotFoundError } from '~/app/core/error/error';
+import { ErrorComponent } from '~/app/core/error/error.component';
 import { CrushRuleService } from '~/app/shared/api/crush-rule.service';
 import { ErasureCodeProfileService } from '~/app/shared/api/erasure-code-profile.service';
 import { PoolService } from '~/app/shared/api/pool.service';
@@ -29,6 +30,7 @@ import { PoolFormInfo } from '~/app/shared/models/pool-form-info';
 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
 import { ModalService } from '~/app/shared/services/modal.service';
 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { SharedModule } from '~/app/shared/shared.module';
 import {
   configureTestBed,
   FixtureHelper,
@@ -51,6 +53,8 @@ describe('PoolFormComponent', () => {
   let router: Router;
   let ecpService: ErasureCodeProfileService;
   let crushRuleService: CrushRuleService;
+  let poolPermissions: Permission;
+  let authStorageService: AuthStorageService;
 
   const setPgNum = (pgs: number): AbstractControl => {
     const control = formHelper.setValue('pgNum', pgs);
@@ -131,11 +135,11 @@ describe('PoolFormComponent', () => {
     formHelper = new FormHelper(form);
   };
 
-  const routes: Routes = [{ path: '404', component: NotFoundComponent }];
+  const routes: Routes = [{ path: '404', component: ErrorComponent }];
 
   configureTestBed(
     {
-      declarations: [NotFoundComponent],
+      declarations: [ErrorComponent],
       imports: [
         BrowserAnimationsModule,
         HttpClientTestingModule,
@@ -143,6 +147,7 @@ describe('PoolFormComponent', () => {
         ToastrModule.forRoot(),
         NgbNavModule,
         PoolModule,
+        SharedModule,
         NgbModalModule
       ],
       providers: [
@@ -167,7 +172,11 @@ describe('PoolFormComponent', () => {
 
     router = TestBed.inject(Router);
     navigationSpy = spyOn(router, 'navigate').and.stub();
-
+    authStorageService = TestBed.inject(AuthStorageService);
+    spyOn(authStorageService, 'getPermissions').and.callFake(() => ({
+      pool: poolPermissions
+    }));
+    poolPermissions = new Permission(['update', 'delete', 'read', 'create']);
     setUpPoolComponent();
 
     component.loadingReady();
@@ -177,57 +186,47 @@ describe('PoolFormComponent', () => {
     expect(component).toBeTruthy();
   });
 
-  describe('redirect not allowed users', () => {
-    let poolPermissions: Permission;
-    let authStorageService: AuthStorageService;
-
-    const expectRedirect = (redirected = true) => {
+  describe('throws error for not allowed users', () => {
+    const expectError = (redirected: boolean) => {
       navigationSpy.calls.reset();
-      component.authenticate();
-      expect(navigationSpy).toHaveBeenCalledTimes(redirected ? 1 : 0);
+      if (redirected) {
+        expect(() => component.authenticate()).toThrowError(DashboardNotFoundError);
+      } else {
+        expect(() => component.authenticate()).not.toThrowError();
+      }
     };
 
     beforeEach(() => {
-      poolPermissions = {
-        create: false,
-        update: false,
-        read: false,
-        delete: false
-      };
-      authStorageService = TestBed.inject(AuthStorageService);
-      spyOn(authStorageService, 'getPermissions').and.callFake(() => ({
-        pool: poolPermissions
-      }));
+      poolPermissions = new Permission(['delete']);
     });
 
-    it('navigates to 404 if not allowed', () => {
-      component.authenticate();
-      expect(router.navigate).toHaveBeenCalledWith(['/404']);
+    it('navigates to Dashboard if not allowed', () => {
+      expect(() => component.authenticate()).toThrowError(DashboardNotFoundError);
     });
 
-    it('navigates if user is not allowed', () => {
-      expectRedirect();
+    it('throws error if user is not allowed', () => {
+      expectError(true);
       poolPermissions.read = true;
-      expectRedirect();
+      expectError(true);
       poolPermissions.delete = true;
-      expectRedirect();
+      expectError(true);
       poolPermissions.update = true;
-      expectRedirect();
+      expectError(true);
       component.editing = true;
       poolPermissions.update = false;
       poolPermissions.create = true;
-      expectRedirect();
+      expectError(true);
     });
 
-    it('does not navigate users with right permissions', () => {
+    it('does not throw error for users with right permissions', () => {
       poolPermissions.read = true;
       poolPermissions.create = true;
-      expectRedirect(false);
+      expectError(false);
       component.editing = true;
       poolPermissions.update = true;
-      expectRedirect(false);
+      expectError(false);
       poolPermissions.create = false;
-      expectRedirect(false);
+      expectError(false);
     });
   });
 
index c1b6ae5db835d471e155160185054fceab5005b3..a3cec3854857101d630c58cbde99302b1c86f6df 100644 (file)
@@ -6,6 +6,7 @@ import { NgbNav, NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
 import _ from 'lodash';
 import { Observable, ReplaySubject, Subscription } from 'rxjs';
 
+import { DashboardNotFoundError } from '~/app/core/error/error';
 import { CrushRuleService } from '~/app/shared/api/crush-rule.service';
 import { ErasureCodeProfileService } from '~/app/shared/api/erasure-code-profile.service';
 import { PoolService } from '~/app/shared/api/pool.service';
@@ -112,7 +113,7 @@ export class PoolFormComponent extends CdForm implements OnInit {
       (!this.permission.update && this.editing) ||
       (!this.permission.create && !this.editing)
     ) {
-      this.router.navigate(['/404']);
+      throw new DashboardNotFoundError();
     }
   }
 
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-501/rgw-501.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-501/rgw-501.component.html
deleted file mode 100644 (file)
index 334d170..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-<cd-alert-panel type="info">
-  {{ message }}<br>
-  <ng-container i18n>Please consult the <cd-doc section="rgw"></cd-doc> on how
-    to configure and enable the Object Gateway management functionality.</ng-container>
-</cd-alert-panel>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-501/rgw-501.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-501/rgw-501.component.scss
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-501/rgw-501.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-501/rgw-501.component.spec.ts
deleted file mode 100644 (file)
index 763cd71..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { RouterTestingModule } from '@angular/router/testing';
-
-import { SharedModule } from '~/app/shared/shared.module';
-import { configureTestBed } from '~/testing/unit-test-helper';
-import { Rgw501Component } from './rgw-501.component';
-
-describe('Rgw501Component', () => {
-  let component: Rgw501Component;
-  let fixture: ComponentFixture<Rgw501Component>;
-
-  configureTestBed({
-    declarations: [Rgw501Component],
-    imports: [HttpClientTestingModule, RouterTestingModule, SharedModule]
-  });
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(Rgw501Component);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-501/rgw-501.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-501/rgw-501.component.ts
deleted file mode 100644 (file)
index 77bea30..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-import { Component, OnDestroy, OnInit } from '@angular/core';
-import { ActivatedRoute } from '@angular/router';
-
-@Component({
-  selector: 'cd-rgw-501',
-  templateUrl: './rgw-501.component.html',
-  styleUrls: ['./rgw-501.component.scss']
-})
-export class Rgw501Component implements OnInit, OnDestroy {
-  message = 'The Object Gateway service is not configured.';
-  routeParamsSubscribe: any;
-
-  constructor(private route: ActivatedRoute) {}
-
-  ngOnInit() {
-    this.routeParamsSubscribe = this.route.params.subscribe((params: { message: string }) => {
-      this.message = params.message;
-    });
-  }
-
-  ngOnDestroy() {
-    this.routeParamsSubscribe.unsubscribe();
-  }
-}
index 00ef8bd49e512acdaaab02d294e07e99c5efa13d..33c6e01560d260c60059ed753897342f63f30d5e 100644 (file)
@@ -7,10 +7,8 @@ import { NgbNavModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
 import { NgxPipeFunctionModule } from 'ngx-pipe-function';
 
 import { ActionLabels, URLVerbs } from '~/app/shared/constants/app.constants';
-import { AuthGuardService } from '~/app/shared/services/auth-guard.service';
 import { SharedModule } from '~/app/shared/shared.module';
 import { PerformanceCounterModule } from '../performance-counter/performance-counter.module';
-import { Rgw501Component } from './rgw-501/rgw-501.component';
 import { RgwBucketDetailsComponent } from './rgw-bucket-details/rgw-bucket-details.component';
 import { RgwBucketFormComponent } from './rgw-bucket-form/rgw-bucket-form.component';
 import { RgwBucketListComponent } from './rgw-bucket-list/rgw-bucket-list.component';
@@ -37,7 +35,6 @@ import { RgwUserSwiftKeyModalComponent } from './rgw-user-swift-key-modal/rgw-us
     NgxPipeFunctionModule
   ],
   exports: [
-    Rgw501Component,
     RgwDaemonListComponent,
     RgwDaemonDetailsComponent,
     RgwBucketFormComponent,
@@ -47,7 +44,6 @@ import { RgwUserSwiftKeyModalComponent } from './rgw-user-swift-key-modal/rgw-us
     RgwUserDetailsComponent
   ],
   declarations: [
-    Rgw501Component,
     RgwDaemonListComponent,
     RgwDaemonDetailsComponent,
     RgwBucketFormComponent,
@@ -105,12 +101,6 @@ const routes: Routes = [
         data: { breadcrumbs: ActionLabels.EDIT }
       }
     ]
-  },
-  {
-    path: '501/:message',
-    component: Rgw501Component,
-    canActivate: [AuthGuardService],
-    data: { breadcrumbs: 'Object Gateway' }
   }
 ];
 
index 56b92e26377a5adda031a453b371d193e4210ea5..74583431c97a829b5337bf9244ac2438e0d7abcf 100644 (file)
@@ -13,7 +13,6 @@ import { LoginComponent } from './login/login.component';
 import { RoleDetailsComponent } from './role-details/role-details.component';
 import { RoleFormComponent } from './role-form/role-form.component';
 import { RoleListComponent } from './role-list/role-list.component';
-import { SsoNotFoundComponent } from './sso/sso-not-found/sso-not-found.component';
 import { UserFormComponent } from './user-form/user-form.component';
 import { UserListComponent } from './user-list/user-list.component';
 import { UserPasswordFormComponent } from './user-password-form/user-password-form.component';
@@ -36,7 +35,6 @@ import { UserTabsComponent } from './user-tabs/user-tabs.component';
     RoleDetailsComponent,
     RoleFormComponent,
     RoleListComponent,
-    SsoNotFoundComponent,
     UserTabsComponent,
     UserListComponent,
     UserFormComponent,
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
deleted file mode 100644 (file)
index b29ede9..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-<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"
-                          [href]="logoutUrl">Login Page</a>. You'll be logged out from the Identity Provider when you retry logging in.</h4>
-
-    <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>
-  </div>
-</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/sso/sso-not-found/sso-not-found.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/sso/sso-not-found/sso-not-found.component.scss
deleted file mode 100644 (file)
index fdf2e71..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-h1 {
-  font-size: -webkit-xxx-large;
-}
-
-* {
-  font-family: monospace;
-}
-
-img {
-  width: 50vw;
-}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/sso/sso-not-found/sso-not-found.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/sso/sso-not-found/sso-not-found.component.spec.ts
deleted file mode 100644 (file)
index 6e3cf5d..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { configureTestBed } from '~/testing/unit-test-helper';
-import { SsoNotFoundComponent } from './sso-not-found.component';
-
-describe('SsoNotFoundComponent', () => {
-  let component: SsoNotFoundComponent;
-  let fixture: ComponentFixture<SsoNotFoundComponent>;
-
-  configureTestBed({
-    declarations: [SsoNotFoundComponent]
-  });
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(SsoNotFoundComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-
-  it('should render the correct logout url', () => {
-    const expectedUrl = `http://localhost/auth/saml2/slo`;
-    const logoutAnchor = fixture.debugElement.nativeElement.querySelector('.sso-logout');
-
-    expect(logoutAnchor.href).toEqual(expectedUrl);
-  });
-});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/sso/sso-not-found/sso-not-found.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/sso/sso-not-found/sso-not-found.component.ts
deleted file mode 100644 (file)
index 24bfcd9..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-import { Component } from '@angular/core';
-
-@Component({
-  selector: 'cd-sso-not-found',
-  templateUrl: './sso-not-found.component.html',
-  styleUrls: ['./sso-not-found.component.scss']
-})
-export class SsoNotFoundComponent {
-  logoutUrl: string;
-
-  constructor() {
-    this.logoutUrl = `${window.location.origin}/auth/saml2/slo`;
-  }
-}
index c220fa6dedf1690925709f2f84dd1ab0da0a6f92..0a5acf317a3a46d7397735161ee78d68c68011eb 100644 (file)
@@ -5,22 +5,20 @@ import { RouterModule } from '@angular/router';
 import { BlockUIModule } from 'ng-block-ui';
 
 import { SharedModule } from '../shared/shared.module';
-import { ForbiddenComponent } from './forbidden/forbidden.component';
+import { ErrorComponent } from './error/error.component';
 import { BlankLayoutComponent } from './layouts/blank-layout/blank-layout.component';
 import { LoginLayoutComponent } from './layouts/login-layout/login-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: [BlockUIModule.forRoot(), CommonModule, NavigationModule, RouterModule, SharedModule],
   exports: [NavigationModule],
   declarations: [
-    NotFoundComponent,
-    ForbiddenComponent,
     WorkbenchLayoutComponent,
     BlankLayoutComponent,
-    LoginLayoutComponent
+    LoginLayoutComponent,
+    ErrorComponent
   ]
 })
 export class CoreModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.html
new file mode 100644 (file)
index 0000000..e41edda
--- /dev/null
@@ -0,0 +1,35 @@
+<head>
+  <title>Error Page</title>
+  <base target="_blank">
+</head>
+<div class="dashboard row">
+  <div class="text-center content">
+    <br>
+    <div *ngIf="header && message; else elseBlock">
+    <i class="{{ icon }}"
+       aria-hidden="true"></i>
+    <br><br><br>
+    <h3><b>{{ header }}</b></h3>
+      <br>
+    <h4>{{ message }}</h4>
+    </div>
+    <ng-template #elseBlock>
+      <i class="fa fa-exclamation-triangle"
+         aria-hidden="true"></i>
+      <br><br><br>
+      <h3 i18n><b>Page not Found</b></h3>
+        <br>
+      <h4 i18n>Sorry, we couldn’t find what you were looking for.
+      The page you requested may have been changed or moved.</h4>
+    </ng-template>
+    <div *ngIf="section">
+      <h4 i18n>Please consult the <a href="{{ docUrl }}">documentation</a> on how to configure and enable
+        the {{ section_info }} management functionality.</h4>
+    </div>
+    <br><br>
+    <div>
+      <button class="btn btn-primary"
+              [routerLink]="'/dashboard'">Go To Dashboard</button>
+    </div>
+  </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.scss
new file mode 100644 (file)
index 0000000..618202d
--- /dev/null
@@ -0,0 +1,47 @@
+@use './src/styles/vendor/variables' as vv;
+
+h4 {
+  color: vv.$gray-700;
+}
+
+i {
+  font-size: 6em;
+  margin-top: 120px;
+}
+
+.text-center {
+  background-color: vv.$body-bg-alt;
+}
+
+.dashboard {
+  background-color: vv.$body-bg-alt;
+  height: 100%;
+  position: relative;
+}
+
+.content {
+  left: 50%;
+  position: absolute;
+  top: 40%;
+  transform: translate(-50%, -50%);
+  width: 100%;
+}
+
+.row {
+  display: block;
+  margin-left: -29px;
+  margin-right: -29px;
+  padding-top: 10em;
+}
+
+.fa-exclamation-triangle {
+  color: vv.$danger;
+}
+
+.fa-lock {
+  color: vv.$danger;
+}
+
+.fa-wrench {
+  color: vv.$info;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.spec.ts
new file mode 100644 (file)
index 0000000..3e9b24e
--- /dev/null
@@ -0,0 +1,47 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { ErrorComponent } from './error.component';
+
+describe('ErrorComponent', () => {
+  let component: ErrorComponent;
+  let fixture: ComponentFixture<ErrorComponent>;
+
+  configureTestBed({
+    declarations: [ErrorComponent],
+    imports: [HttpClientTestingModule, RouterTestingModule, SharedModule]
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(ErrorComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('should show error message and header', () => {
+    window.history.pushState({ message: 'Access Forbidden', header: 'User Denied' }, 'Errors');
+    component.fetchData();
+    fixture.detectChanges();
+    const header = fixture.debugElement.nativeElement.querySelector('h3');
+    expect(header.innerHTML).toContain('User Denied');
+    const message = fixture.debugElement.nativeElement.querySelector('h4');
+    expect(message.innerHTML).toContain('Access Forbidden');
+  });
+
+  it('should show 404 Page not Found if message and header are blank', () => {
+    window.history.pushState({ message: '', header: '' }, 'Errors');
+    component.fetchData();
+    fixture.detectChanges();
+    const header = fixture.debugElement.nativeElement.querySelector('h3');
+    expect(header.innerHTML).toContain('Page not Found');
+    const message = fixture.debugElement.nativeElement.querySelector('h4');
+    expect(message.innerHTML).toContain('Sorry, we couldn’t find what you were looking for.');
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.ts
new file mode 100644 (file)
index 0000000..1f2cf57
--- /dev/null
@@ -0,0 +1,59 @@
+import { Component, HostListener, OnDestroy, OnInit } from '@angular/core';
+import { NavigationEnd, Router, RouterEvent } from '@angular/router';
+
+import { Subscription } from 'rxjs';
+import { filter } from 'rxjs/operators';
+
+import { DocService } from '~/app/shared/services/doc.service';
+
+@Component({
+  selector: 'cd-error',
+  templateUrl: './error.component.html',
+  styleUrls: ['./error.component.scss']
+})
+export class ErrorComponent implements OnDestroy, OnInit {
+  header: string;
+  message: string;
+  section: string;
+  section_info: string;
+  icon: string;
+  docUrl: string;
+  source: string;
+  routerSubscription: Subscription;
+
+  constructor(private router: Router, private docService: DocService) {}
+
+  ngOnInit() {
+    this.fetchData();
+    this.routerSubscription = this.router.events
+      .pipe(filter((event: RouterEvent) => event instanceof NavigationEnd))
+      .subscribe(() => {
+        this.fetchData();
+      });
+  }
+
+  @HostListener('window:beforeunload', ['$event']) unloadHandler(event: Event) {
+    event.returnValue = false;
+  }
+
+  fetchData() {
+    try {
+      this.router.onSameUrlNavigation = 'reload';
+      this.message = history.state.message;
+      this.header = history.state.header;
+      this.section = history.state.section;
+      this.section_info = history.state.section_info;
+      this.icon = history.state.icon;
+      this.source = history.state.source;
+      this.docUrl = this.docService.urlGenerator(this.section);
+    } catch (error) {
+      this.router.navigate(['/error']);
+    }
+  }
+
+  ngOnDestroy() {
+    if (this.routerSubscription) {
+      this.routerSubscription.unsubscribe();
+    }
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.ts
new file mode 100644 (file)
index 0000000..0270a45
--- /dev/null
@@ -0,0 +1,27 @@
+import { Icons } from '~/app/shared/enum/icons.enum';
+
+export class DashboardError extends Error {
+  header: string;
+  message: string;
+  icon: string;
+}
+
+export class DashboardNotFoundError extends DashboardError {
+  header = $localize`Page Not Found`;
+  message = $localize`Sorry, we couldn’t find what you were looking for.
+  The page you requested may have been changed or moved.`;
+  icon = Icons.warning;
+}
+
+export class DashboardForbiddenError extends DashboardError {
+  header = $localize`Access Denied`;
+  message = $localize`Sorry, you don’t have permission to view this page or resource.`;
+  icon = Icons.lock;
+}
+
+export class DashboardUserDeniedError extends DashboardError {
+  header = $localize`User Denied`;
+  message = $localize`Sorry, the user does not exist in Ceph.
+  You'll be logged out from the Identity Provider when you retry logging in.`;
+  icon = Icons.warning;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/forbidden/forbidden.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/forbidden/forbidden.component.html
deleted file mode 100644 (file)
index 5865bd0..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-<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>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/forbidden/forbidden.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/forbidden/forbidden.component.scss
deleted file mode 100644 (file)
index f320c2b..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-h1 {
-  font-family: monospace;
-  font-size: -webkit-xxx-large;
-}
-
-h2 {
-  font-family: monospace;
-  font-size: xx-large;
-}
-
-i {
-  font-size: 16.667rem;
-}
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
deleted file mode 100644 (file)
index 75d3778..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-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';
-
-describe('ForbiddenComponent', () => {
-  let component: ForbiddenComponent;
-  let fixture: ComponentFixture<ForbiddenComponent>;
-
-  configureTestBed({
-    declarations: [ForbiddenComponent],
-    imports: [RouterTestingModule]
-  });
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(ForbiddenComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/forbidden/forbidden.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/forbidden/forbidden.component.ts
deleted file mode 100644 (file)
index 6a77f18..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-import { Component } from '@angular/core';
-
-import { Icons } from '~/app/shared/enum/icons.enum';
-
-@Component({
-  selector: 'cd-forbidden',
-  templateUrl: './forbidden.component.html',
-  styleUrls: ['./forbidden.component.scss']
-})
-export class ForbiddenComponent {
-  icons = Icons;
-}
index 1c3947873e2152bef6c86582efc81cd6e945c15a..dfc7912d8156ddea4df6f09ca3393af84973a1dc 100644 (file)
@@ -1,7 +1,7 @@
 @use './src/styles/vendor/variables' as vv;
 
 .dashboard {
-  background-color: vv.$gray-200;
+  background-color: vv.$body-bg-alt;
   margin: 0;
   padding: 0;
 }
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
deleted file mode 100644 (file)
index cf2da0b..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-<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/1500px-Southern_Keeled_Octopus.jpg">
-    <br>
-    <span>
-      "<a href="https://collections.museumvictoria.com.au/species/8696">Southern Keeled Octopus, Octopus berrima</a>" by Museums Victoria
-      (Photographer: Julian Finn) is licensed under
-      <a rel="nofollow"
-         class="external text"
-         href="https://creativecommons.org/licenses/by/4.0">CC BY 4.0</a>
-    </span>
-    <br>
-    <button type="button"
-            class="btn btn-primary"
-            [routerLink]="'/login'">
-      <ng-container i18n>Back</ng-container>
-    </button>
-  </div>
-</div>
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
deleted file mode 100644 (file)
index b564419..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-h1 {
-  font-family: monospace;
-  font-size: -webkit-xxx-large;
-}
-
-* {
-  font-family: monospace;
-}
-
-img {
-  width: 50vw;
-}
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
deleted file mode 100644 (file)
index 7d64730..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-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';
-
-describe('NotFoundComponent', () => {
-  let component: NotFoundComponent;
-  let fixture: ComponentFixture<NotFoundComponent>;
-
-  configureTestBed({
-    declarations: [NotFoundComponent],
-    imports: [RouterTestingModule]
-  });
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(NotFoundComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.ts
deleted file mode 100644 (file)
index 1b8d6b8..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-import { Component } from '@angular/core';
-
-@Component({
-  selector: 'cd-not-found',
-  templateUrl: './not-found.component.html',
-  styleUrls: ['./not-found.component.scss']
-})
-export class NotFoundComponent {}
index 4fe6d98aca9b583cc04e24774a712f1eedd98bb1..9a07d4dec94c91e0239c0231f5c68b4006dcd3e4 100644 (file)
@@ -24,7 +24,8 @@ export enum Icons {
   left = 'fa fa-arrow-left', // Mark out
   right = 'fa fa-arrow-right', // Mark in
   down = 'fa fa-arrow-down', // Mark Down
-  erase = 'fa fa-eraser', // Purge
+  erase = 'fa fa-eraser', // Purge  color: bd.$white;
+
   user = 'fa fa-user', // User, Initiators
   users = 'fa fa-users', // Users, Groups
   share = 'fa fa-share-alt', // share
@@ -64,6 +65,7 @@ export enum Icons {
   close = 'fa fa-times', // Close
   json = 'fa fa-file-code-o', // JSON file
   text = 'fa fa-file-text', // Text file
+  wrench = 'fa fa-wrench', // Configuration Error
 
   /* Icons for special effect */
   large = 'fa fa-lg', // icon becomes 33% larger
index 092708fe3c7c8645c4550b80707bd078abce7312..ba7c30f490e239b917c4e140bc651ccf40600b5c 100644 (file)
@@ -110,7 +110,7 @@ describe('ApiInterceptorService', () => {
         {
           status: 403
         },
-        [['/403']]
+        [['error'], {'state': {'header': 'Access Denied', 'icon': 'fa fa-lock', 'message': 'Sorry, you don’t have permission to view this page or resource.', 'source': 'forbidden'}}] // prettier-ignore
       );
     });
 
index d01def3b5a76da75b30276e4c89df3fd2770f12b..cf7ddd342e0ea3b024a31332e8acdee0bd01ab4c 100644 (file)
@@ -73,7 +73,14 @@ export class ApiInterceptorService implements HttpInterceptor {
               this.router.navigate(['/login']);
               break;
             case 403:
-              this.router.navigate(['/403']);
+              this.router.navigate(['error'], {
+                state: {
+                  message: $localize`Sorry, you don’t have permission to view this page or resource.`,
+                  header: $localize`Access Denied`,
+                  icon: 'fa fa-lock',
+                  source: 'forbidden'
+                }
+              });
               break;
             default:
               timeoutId = this.prepareNotification(resp);
index d657f26b1e6888272ce7d1bbee12238cb3d3b0cd..883139986d62a5febc470dcbb7c5ac83ab67d521 100644 (file)
@@ -5,6 +5,7 @@ import { RouterTestingModule } from '@angular/router/testing';
 
 import { of as observableOf } from 'rxjs';
 
+import { DashboardNotFoundError } from '~/app/core/error/error';
 import { configureTestBed } from '~/testing/unit-test-helper';
 import { FeatureTogglesGuardService } from './feature-toggles-guard.service';
 import { FeatureTogglesService } from './feature-toggles.service';
@@ -65,8 +66,7 @@ describe('FeatureTogglesGuardService', () => {
     expect(router.url).toBe('/');
   }));
 
-  it('should redirect to 404 if disable', fakeAsync(() => {
-    expect(testCanActivate('cephfs', { cephfs: false })).toBe(false);
-    expect(router.url).toBe('/404');
+  it('should throw error if disable', fakeAsync(() => {
+    expect(() => testCanActivate('cephfs', { cephfs: false })).toThrowError(DashboardNotFoundError);
   }));
 });
index 3cad644d91cefcc05708ebdaf88c8f9d6cc74f6f..ad94f2689341f09d3d35c2103ab822a2e73f67bc 100644 (file)
@@ -1,21 +1,22 @@
 import { Injectable } from '@angular/core';
-import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, Router } from '@angular/router';
+import { ActivatedRouteSnapshot, CanActivate, CanActivateChild } from '@angular/router';
 
 import { map } from 'rxjs/operators';
 
+import { DashboardNotFoundError } from '~/app/core/error/error';
 import { FeatureTogglesMap, FeatureTogglesService } from './feature-toggles.service';
 
 @Injectable({
   providedIn: 'root'
 })
 export class FeatureTogglesGuardService implements CanActivate, CanActivateChild {
-  constructor(private router: Router, private featureToggles: FeatureTogglesService) {}
+  constructor(private featureToggles: FeatureTogglesService) {}
 
   canActivate(route: ActivatedRouteSnapshot) {
     return this.featureToggles.get().pipe(
       map((enabledFeatures: FeatureTogglesMap) => {
         if (enabledFeatures[route.routeConfig.path] === false) {
-          this.router.navigate(['404']);
+          throw new DashboardNotFoundError();
           return false;
         }
         return true;
index f7c45aa78ac7b949bed9b03fcda358465692a41a..de42d005e08285d326d11037f480ce4822c236df 100644 (file)
@@ -1,10 +1,12 @@
 import { ErrorHandler, Injectable, Injector } from '@angular/core';
+import { Router } from '@angular/router';
 
+import { DashboardError } from '~/app/core/error/error';
 import { LoggingService } from '../api/logging.service';
 
 @Injectable()
 export class JsErrorHandler implements ErrorHandler {
-  constructor(private injector: Injector) {}
+  constructor(private injector: Injector, private router: Router) {}
 
   handleError(error: any) {
     const loggingService = this.injector.get(LoggingService);
@@ -12,6 +14,20 @@ export class JsErrorHandler implements ErrorHandler {
     const message = error && error.message;
     const stack = error && error.stack;
     loggingService.jsError(url, message, stack).subscribe();
-    throw error;
+    if (error.rejection instanceof DashboardError) {
+      setTimeout(
+        () =>
+          this.router.navigate(['error'], {
+            state: {
+              message: error.rejection.message,
+              header: error.rejection.header,
+              icon: error.rejection.icon
+            }
+          }),
+        50
+      );
+    } else {
+      throw error;
+    }
   }
 }
index cdaae28af3845d447eb9c6e9d85c207432eec61e..0948fc878a9e0eb77bed08c016ba60d06e2bcc29 100644 (file)
@@ -70,7 +70,7 @@ describe('ModuleStatusGuardService', () => {
   }));
 
   it('should test canActivateChild with status unavailable', fakeAsync(() => {
-    testCanActivate({ available: false, message: null }, false, '/foo/');
+    testCanActivate({ available: false, message: null }, false, '/foo');
   }));
 
   it('should test canActivateChild with status unavailable', fakeAsync(() => {
index 606cddb9ac84fa5c2ac037375cd70ff6c9cd851d..171f34adfe6b48e09f6ba6aeceeabb08032509e3 100644 (file)
@@ -5,6 +5,8 @@ import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, Router } from '@
 import { of as observableOf } from 'rxjs';
 import { catchError, map } from 'rxjs/operators';
 
+import { Icons } from '../enum/icons.enum';
+
 /**
  * This service checks if a route can be activated by executing a
  * REST API call to '/api/<apiPath>/status'. If the returned response
@@ -55,7 +57,15 @@ export class ModuleStatusGuardService implements CanActivate, CanActivateChild {
     return this.http.get(`api/${config.apiPath}/status`).pipe(
       map((resp: any) => {
         if (!resp.available) {
-          this.router.navigate([config.redirectTo, resp.message || '']);
+          this.router.navigate([config.redirectTo || ''], {
+            state: {
+              header: config.header,
+              message: resp.message,
+              section: config.section,
+              section_info: config.section_info,
+              icon: Icons.wrench
+            }
+          });
         }
         return resp.available;
       }),
index 1d2c0833624776998d465d27e57ef6b0c8c3169a..9a330cdc83eb42f03cda1f64828ba0ab98fbfe28 100644 (file)
@@ -1,8 +1,9 @@
 import { Component, NgZone } from '@angular/core';
 import { fakeAsync, TestBed, tick } from '@angular/core/testing';
-import { Router, Routes } from '@angular/router';
+import { Routes } from '@angular/router';
 import { RouterTestingModule } from '@angular/router/testing';
 
+import { DashboardUserDeniedError } from '~/app/core/error/error';
 import { configureTestBed } from '~/testing/unit-test-helper';
 import { AuthStorageService } from './auth-storage.service';
 import { NoSsoGuardService } from './no-sso-guard.service';
@@ -11,7 +12,6 @@ describe('NoSsoGuardService', () => {
   let service: NoSsoGuardService;
   let authStorageService: AuthStorageService;
   let ngZone: NgZone;
-  let router: Router;
 
   @Component({ selector: 'cd-404', template: '' })
   class NotFoundComponent {}
@@ -28,7 +28,6 @@ describe('NoSsoGuardService', () => {
     service = TestBed.inject(NoSsoGuardService);
     authStorageService = TestBed.inject(AuthStorageService);
     ngZone = TestBed.inject(NgZone);
-    router = TestBed.inject(Router);
   });
 
   it('should be created', () => {
@@ -43,9 +42,8 @@ describe('NoSsoGuardService', () => {
   it('should prevent if logged in via SSO', fakeAsync(() => {
     spyOn(authStorageService, 'isSSO').and.returnValue(true);
     ngZone.run(() => {
-      expect(service.canActivate()).toBe(false);
+      expect(() => service.canActivate()).toThrowError(DashboardUserDeniedError);
     });
     tick();
-    expect(router.url).toBe('/404');
   }));
 });
index 79338a8541becae072ed23ad4a8f87c0a8fda05e..d4abcde0dd7b556ad9333699be8fb25209a51d02 100644 (file)
@@ -1,6 +1,7 @@
 import { Injectable } from '@angular/core';
-import { CanActivate, CanActivateChild, Router } from '@angular/router';
+import { CanActivate, CanActivateChild } from '@angular/router';
 
+import { DashboardUserDeniedError } from '~/app/core/error/error';
 import { AuthStorageService } from './auth-storage.service';
 
 /**
@@ -11,13 +12,13 @@ import { AuthStorageService } from './auth-storage.service';
   providedIn: 'root'
 })
 export class NoSsoGuardService implements CanActivate, CanActivateChild {
-  constructor(private authStorageService: AuthStorageService, private router: Router) {}
+  constructor(private authStorageService: AuthStorageService) {}
 
   canActivate() {
     if (!this.authStorageService.isSSO()) {
       return true;
     }
-    this.router.navigate(['404']);
+    throw new DashboardUserDeniedError();
     return false;
   }
 
diff --git a/src/pybind/mgr/dashboard/frontend/src/assets/1500px-Southern_Keeled_Octopus.jpg b/src/pybind/mgr/dashboard/frontend/src/assets/1500px-Southern_Keeled_Octopus.jpg
deleted file mode 100644 (file)
index 3e31c90..0000000
Binary files a/src/pybind/mgr/dashboard/frontend/src/assets/1500px-Southern_Keeled_Octopus.jpg and /dev/null differ
index 4f40aa73971cc3c2499eb8d4d7a33872e905a77c..676b0cae770ecba7ecb31e737cdfb8f2806e2b16 100644 (file)
@@ -49,6 +49,7 @@ $theme-colors: (
 $body-color-bright: $light !default;
 $body-bg: $white !default;
 $body-color: $gray-900 !default;
+$body-bg-alt: $gray-200 !default;
 
 // Typography
 
index 9e7d58a69bf3681490b96772756a0be241af9367..291c39d91f1951d012070b2419c49b612c3f56f4 100644 (file)
@@ -29,7 +29,7 @@ class OrchestratorAPI(OrchestratorClientMixin):
         except (RuntimeError, OrchestratorError, ImportError) as e:
             return dict(
                 available=False,
-                message='Orchestrator is unavailable for unknown reason: {}'.format(str(e)))
+                message='Orchestrator is unavailable: {}'.format(str(e)))
 
     def orchestrator_wait(self, completions):
         return self._orchestrator_wait(completions)