From: Aashish Sharma Date: Tue, 27 Oct 2020 07:06:24 +0000 (+0530) Subject: mgr/dashboard: new generic HTTP error page component X-Git-Tag: v17.0.0~288^2~1 X-Git-Url: http://git.apps.os.sepia.ceph.com/?a=commitdiff_plain;h=13900cf6aef403af35cf9e6a80ef4f706f93e722;p=ceph-ci.git mgr/dashboard: new generic HTTP error page component 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 --- diff --git a/doc/dev/developer_guide/dash-devel.rst b/doc/dev/developer_guide/dash-devel.rst index 5401cf9d133..130a6b9a65d 100644 --- a/doc/dev/developer_guide/dash-devel.rst +++ b/doc/dev/developer_guide/dash-devel.rst @@ -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 index 0864a5edcc1..00000000000 --- a/src/pybind/mgr/dashboard/frontend/cypress/integration/nfs/nfs.e2e-spec.ts +++ /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 index 7dd482a140c..00000000000 --- a/src/pybind/mgr/dashboard/frontend/cypress/integration/nfs/nfs.po.ts +++ /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' } }; -} diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/page-helper.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/page-helper.po.ts index 38edfc45160..7b099fd98ac 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/integration/page-helper.po.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/page-helper.po.ts @@ -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) { diff --git a/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/navigation.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/navigation.po.ts index a26a5ff10c4..486d30dd646 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/navigation.po.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/integration/ui/navigation.po.ts @@ -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' }, 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 f9614c9c577..4ca71f17259 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 @@ -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' }] } ]; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.spec.ts index 40f6937c66b..136375cbbbe 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.spec.ts @@ -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); }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.ts index 3ffa5caf7d6..b3bc1401082 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/silence-form/silence-form.component.ts @@ -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 index bcbb8812542..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-501/nfs-501.component.html +++ /dev/null @@ -1,5 +0,0 @@ - - {{ message }}
- Please consult the on how - to configure and enable the NFS Ganesha management functionality. -
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 index e69de29bb2d..00000000000 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 index e23b5f76819..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-501/nfs-501.component.spec.ts +++ /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; - - 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 index 7f917229465..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-501/nfs-501.component.ts +++ /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(); - } -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs.module.ts index b19b742cea1..4205eb63b26 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs.module.ts @@ -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 {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.spec.ts index 8f63446d5d4..1d58a1778ce 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.spec.ts @@ -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); }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.ts index c1b6ae5db83..a3cec385485 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.ts @@ -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 index 334d1703b0e..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-501/rgw-501.component.html +++ /dev/null @@ -1,5 +0,0 @@ - - {{ message }}
- Please consult the on how - to configure and enable the Object Gateway management functionality. -
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 index e69de29bb2d..00000000000 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 index 763cd71dbab..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-501/rgw-501.component.spec.ts +++ /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; - - 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 index 77bea30a6be..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-501/rgw-501.component.ts +++ /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(); - } -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts index 00ef8bd49e5..33c6e01560d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts @@ -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' } } ]; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/auth.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/auth.module.ts index 56b92e26377..74583431c97 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/auth.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/auth.module.ts @@ -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 index b29ede92f8e..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/sso/sso-not-found/sso-not-found.component.html +++ /dev/null @@ -1,17 +0,0 @@ -
-
-

Sorry, the user does not exist in Ceph.

-

Return to Login Page. You'll be logged out from the Identity Provider when you retry logging in.

- - -
- - "Nautilus Octopus" by Jin Kemoole is licensed under - CC BY 2.0 - -
-
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 index fdf2e7100f6..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/sso/sso-not-found/sso-not-found.component.scss +++ /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 index 6e3cf5dc257..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/sso/sso-not-found/sso-not-found.component.spec.ts +++ /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; - - 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 index 24bfcd94b25..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/sso/sso-not-found/sso-not-found.component.ts +++ /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`; - } -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/core.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/core.module.ts index c220fa6dedf..0a5acf317a3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/core.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/core.module.ts @@ -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 index 00000000000..e41edda9651 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.html @@ -0,0 +1,35 @@ + + Error Page + + +
+
+
+
+ +


+

{{ header }}

+
+

{{ message }}

+
+ + +


+

Page not Found

+
+

Sorry, we couldn’t find what you were looking for. + The page you requested may have been changed or moved.

+
+
+

Please consult the documentation on how to configure and enable + the {{ section_info }} management functionality.

+
+

+
+ +
+
+
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 index 00000000000..618202da8bd --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.scss @@ -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 index 00000000000..3e9b24ebf8e --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.spec.ts @@ -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; + + 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 index 00000000000..1f2cf57c508 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.ts @@ -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 index 00000000000..0270a458713 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.ts @@ -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 index 5865bd02a1b..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/forbidden/forbidden.component.html +++ /dev/null @@ -1,12 +0,0 @@ -
-
-

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.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/forbidden/forbidden.component.scss deleted file mode 100644 index f320c2b5839..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/forbidden/forbidden.component.scss +++ /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 index 75d3778f8fc..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/forbidden/forbidden.component.spec.ts +++ /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; - - 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 index 6a77f18d541..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/forbidden/forbidden.component.ts +++ /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; -} 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 index 1c3947873e2..dfc7912d815 100644 --- 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 @@ -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 index cf2da0b334c..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.html +++ /dev/null @@ -1,21 +0,0 @@ -
-
-

Sorry, we could not find what you were looking for.

- -
- - "Southern Keeled Octopus, Octopus berrima" by Museums Victoria - (Photographer: Julian Finn) is licensed under - CC BY 4.0 - -
- -
-
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 index b564419b782..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.scss +++ /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 index 7d64730181f..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.spec.ts +++ /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; - - 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 index 1b8d6b807dc..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/not-found/not-found.component.ts +++ /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 {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts index 4fe6d98aca9..9a07d4dec94 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts @@ -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 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/api-interceptor.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/api-interceptor.service.spec.ts index 092708fe3c7..ba7c30f490e 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/api-interceptor.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/api-interceptor.service.spec.ts @@ -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 ); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/api-interceptor.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/api-interceptor.service.ts index d01def3b5a7..cf7ddd342e0 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/api-interceptor.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/api-interceptor.service.ts @@ -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); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/feature-toggles-guard.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/feature-toggles-guard.service.spec.ts index d657f26b1e6..883139986d6 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/feature-toggles-guard.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/feature-toggles-guard.service.spec.ts @@ -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); })); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/feature-toggles-guard.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/feature-toggles-guard.service.ts index 3cad644d91c..ad94f268934 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/feature-toggles-guard.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/feature-toggles-guard.service.ts @@ -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; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/js-error-handler.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/js-error-handler.service.ts index f7c45aa78ac..de42d005e08 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/js-error-handler.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/js-error-handler.service.ts @@ -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; + } } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/module-status-guard.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/module-status-guard.service.spec.ts index cdaae28af38..0948fc878a9 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/module-status-guard.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/module-status-guard.service.spec.ts @@ -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(() => { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/module-status-guard.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/module-status-guard.service.ts index 606cddb9ac8..171f34adfe6 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/module-status-guard.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/module-status-guard.service.ts @@ -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//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; }), diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/no-sso-guard.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/no-sso-guard.service.spec.ts index 1d2c0833624..9a330cdc83e 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/no-sso-guard.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/no-sso-guard.service.spec.ts @@ -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'); })); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/no-sso-guard.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/no-sso-guard.service.ts index 79338a8541b..d4abcde0dd7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/no-sso-guard.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/no-sso-guard.service.ts @@ -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 index 3e31c90594b..00000000000 Binary files a/src/pybind/mgr/dashboard/frontend/src/assets/1500px-Southern_Keeled_Octopus.jpg and /dev/null differ diff --git a/src/pybind/mgr/dashboard/frontend/src/styles/defaults/_bootstrap-defaults.scss b/src/pybind/mgr/dashboard/frontend/src/styles/defaults/_bootstrap-defaults.scss index 4f40aa73971..676b0cae770 100644 --- a/src/pybind/mgr/dashboard/frontend/src/styles/defaults/_bootstrap-defaults.scss +++ b/src/pybind/mgr/dashboard/frontend/src/styles/defaults/_bootstrap-defaults.scss @@ -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 diff --git a/src/pybind/mgr/dashboard/services/orchestrator.py b/src/pybind/mgr/dashboard/services/orchestrator.py index 9e7d58a69bf..291c39d91f1 100644 --- a/src/pybind/mgr/dashboard/services/orchestrator.py +++ b/src/pybind/mgr/dashboard/services/orchestrator.py @@ -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)