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::
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
**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
.....
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
----
+++ /dev/null
-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');
- });
- });
-});
+++ /dev/null
-import { PageHelper } from '../page-helper.po';
-
-export class NfsPageHelper extends PageHelper {
- pages = { index: { url: '#/nfs', id: 'cd-nfs-501' } };
-}
}
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) {
};
navigations = [
- { menu: 'NFS', component: 'cd-nfs-501' },
+ { menu: 'NFS', component: 'cd-error' },
{
menu: 'Object Gateway',
submenus: [
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' },
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';
canActivateChild: [AuthGuardService, ChangePasswordGuardService],
children: [
{ path: 'dashboard', component: DashboardComponent },
+ { path: 'error', component: ErrorComponent },
+
// Cluster
{
path: 'hosts',
},
{
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 },
{
},
{
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',
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',
]
},
// 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'
},
{
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' }]
}
];
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';
// 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),
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;
);
});
- 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);
});
});
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';
const allowed =
this.permission.read && (this.edit ? this.permission.update : this.permission.create);
if (!allowed) {
- this.router.navigate(['/404']);
+ throw new DashboardNotFoundError();
}
}
+++ /dev/null
-<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>
+++ /dev/null
-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();
- });
-});
+++ /dev/null
-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();
- }
-}
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';
NgbTypeaheadModule,
NgbTooltipModule
],
- declarations: [
- NfsListComponent,
- NfsDetailsComponent,
- NfsFormComponent,
- NfsFormClientComponent,
- Nfs501Component
- ],
- exports: [Nfs501Component]
+ declarations: [NfsListComponent, NfsDetailsComponent, NfsFormComponent, NfsFormClientComponent]
})
export class NfsModule {}
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';
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,
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);
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,
ToastrModule.forRoot(),
NgbNavModule,
PoolModule,
+ SharedModule,
NgbModalModule
],
providers: [
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();
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);
});
});
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';
(!this.permission.update && this.editing) ||
(!this.permission.create && !this.editing)
) {
- this.router.navigate(['/404']);
+ throw new DashboardNotFoundError();
}
}
+++ /dev/null
-<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>
+++ /dev/null
-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();
- });
-});
+++ /dev/null
-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();
- }
-}
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';
NgxPipeFunctionModule
],
exports: [
- Rgw501Component,
RgwDaemonListComponent,
RgwDaemonDetailsComponent,
RgwBucketFormComponent,
RgwUserDetailsComponent
],
declarations: [
- Rgw501Component,
RgwDaemonListComponent,
RgwDaemonDetailsComponent,
RgwBucketFormComponent,
data: { breadcrumbs: ActionLabels.EDIT }
}
]
- },
- {
- path: '501/:message',
- component: Rgw501Component,
- canActivate: [AuthGuardService],
- data: { breadcrumbs: 'Object Gateway' }
}
];
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';
RoleDetailsComponent,
RoleFormComponent,
RoleListComponent,
- SsoNotFoundComponent,
UserTabsComponent,
UserListComponent,
UserFormComponent,
+++ /dev/null
-<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>
+++ /dev/null
-h1 {
- font-size: -webkit-xxx-large;
-}
-
-* {
- font-family: monospace;
-}
-
-img {
- width: 50vw;
-}
+++ /dev/null
-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);
- });
-});
+++ /dev/null
-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`;
- }
-}
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 {}
--- /dev/null
+<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>
--- /dev/null
+@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;
+}
--- /dev/null
+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.');
+ });
+});
--- /dev/null
+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();
+ }
+ }
+}
--- /dev/null
+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;
+}
+++ /dev/null
-<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>
+++ /dev/null
-h1 {
- font-family: monospace;
- font-size: -webkit-xxx-large;
-}
-
-h2 {
- font-family: monospace;
- font-size: xx-large;
-}
-
-i {
- font-size: 16.667rem;
-}
+++ /dev/null
-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();
- });
-});
+++ /dev/null
-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;
-}
@use './src/styles/vendor/variables' as vv;
.dashboard {
- background-color: vv.$gray-200;
+ background-color: vv.$body-bg-alt;
margin: 0;
padding: 0;
}
+++ /dev/null
-<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>
+++ /dev/null
-h1 {
- font-family: monospace;
- font-size: -webkit-xxx-large;
-}
-
-* {
- font-family: monospace;
-}
-
-img {
- width: 50vw;
-}
+++ /dev/null
-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();
- });
-});
+++ /dev/null
-import { Component } from '@angular/core';
-
-@Component({
- selector: 'cd-not-found',
- templateUrl: './not-found.component.html',
- styleUrls: ['./not-found.component.scss']
-})
-export class NotFoundComponent {}
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
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
{
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
);
});
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);
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';
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);
}));
});
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;
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);
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;
+ }
}
}
}));
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(() => {
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
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;
}),
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';
let service: NoSsoGuardService;
let authStorageService: AuthStorageService;
let ngZone: NgZone;
- let router: Router;
@Component({ selector: 'cd-404', template: '' })
class NotFoundComponent {}
service = TestBed.inject(NoSsoGuardService);
authStorageService = TestBed.inject(AuthStorageService);
ngZone = TestBed.inject(NgZone);
- router = TestBed.inject(Router);
});
it('should be created', () => {
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');
}));
});
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';
/**
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;
}
$body-color-bright: $light !default;
$body-bg: $white !default;
$body-color: $gray-900 !default;
+$body-bg-alt: $gray-200 !default;
// Typography
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)