mirror of
https://github.com/ceph/ceph
synced 2025-04-01 23:02:17 +00:00
mgr/dashboard: Create Cluster Workflow welcome screen and e2e tests
A module option called CLUSTER_STATUS has two option. INSTALLED AND POST_INSTALLED. When CLUSTER_STATUS is INSTALLED it will allow to show the create-cluster-wizard after login the initial time. After the cluster creation is succesfull this option is set to POST_INSTALLED Also has the e2e codes for the Review Section Fixes: https://tracker.ceph.com/issues/50336 Signed-off-by: Avan Thakkar <athakkar@redhat.com> Signed-off-by: Nizamudeen A <nia@redhat.com>
This commit is contained in:
parent
f41eae16af
commit
b9f38cadc4
@ -335,7 +335,8 @@ class AuthTest(DashboardTestCase):
|
||||
self.assertStatus(200)
|
||||
data = self.jsonBody()
|
||||
self.assertSchema(data, JObj(sub_elems={
|
||||
"login_url": JLeaf(str)
|
||||
"login_url": JLeaf(str),
|
||||
"cluster_status": JLeaf(str)
|
||||
}, allow_unknown=False))
|
||||
self.logout()
|
||||
|
||||
@ -345,6 +346,7 @@ class AuthTest(DashboardTestCase):
|
||||
self.assertStatus(200)
|
||||
data = self.jsonBody()
|
||||
self.assertSchema(data, JObj(sub_elems={
|
||||
"login_url": JLeaf(str)
|
||||
"login_url": JLeaf(str),
|
||||
"cluster_status": JLeaf(str)
|
||||
}, allow_unknown=False))
|
||||
self.logout(set_cookies=True)
|
||||
|
23
qa/tasks/mgr/dashboard/test_cluster.py
Normal file
23
qa/tasks/mgr/dashboard/test_cluster.py
Normal file
@ -0,0 +1,23 @@
|
||||
from .helper import DashboardTestCase, JLeaf, JObj
|
||||
|
||||
|
||||
class ClusterTest(DashboardTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.reset_session()
|
||||
|
||||
def test_get_status(self):
|
||||
data = self._get('/api/cluster', version='0.1')
|
||||
self.assertStatus(200)
|
||||
self.assertSchema(data, JObj(sub_elems={
|
||||
"status": JLeaf(str)
|
||||
}, allow_unknown=False))
|
||||
|
||||
def test_update_status(self):
|
||||
req = {'status': 'POST_INSTALLED'}
|
||||
self._put('/api/cluster', req, version='0.1')
|
||||
self.assertStatus(200)
|
||||
data = self._get('/api/cluster', version='0.1')
|
||||
self.assertStatus(200)
|
||||
self.assertEqual(data, req)
|
@ -7,6 +7,7 @@ import sys
|
||||
from .. import mgr
|
||||
from ..exceptions import InvalidCredentialsError, UserDoesNotExist
|
||||
from ..services.auth import AuthManager, JwtManager
|
||||
from ..services.cluster import ClusterModel
|
||||
from ..settings import Settings
|
||||
from . import APIDoc, APIRouter, ControllerAuthMixin, EndpointDoc, RESTController, allow_empty_body
|
||||
|
||||
@ -117,4 +118,5 @@ class Auth(RESTController, ControllerAuthMixin):
|
||||
}
|
||||
return {
|
||||
'login_url': self._get_login_url(),
|
||||
'cluster_status': ClusterModel.from_db().dict()['status']
|
||||
}
|
||||
|
20
src/pybind/mgr/dashboard/controllers/cluster.py
Normal file
20
src/pybind/mgr/dashboard/controllers/cluster.py
Normal file
@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from ..security import Scope
|
||||
from ..services.cluster import ClusterModel
|
||||
from . import ApiController, ControllerDoc, EndpointDoc, RESTController
|
||||
|
||||
|
||||
@ApiController('/cluster', Scope.CONFIG_OPT)
|
||||
@ControllerDoc("Get Cluster Details", "Cluster")
|
||||
class Cluster(RESTController):
|
||||
@RESTController.MethodMap(version='0.1')
|
||||
@EndpointDoc("Get the cluster status")
|
||||
def list(self):
|
||||
return ClusterModel.from_db().dict()
|
||||
|
||||
@RESTController.MethodMap(version='0.1')
|
||||
@EndpointDoc("Update the cluster status",
|
||||
parameters={'status': (str, 'Cluster Status')})
|
||||
def singleton_set(self, status: str):
|
||||
ClusterModel(status).to_db()
|
@ -0,0 +1,22 @@
|
||||
import { PageHelper } from '../page-helper.po';
|
||||
import { NotificationSidebarPageHelper } from '../ui/notification.po';
|
||||
|
||||
export class CreateClusterWelcomePageHelper extends PageHelper {
|
||||
pages = {
|
||||
index: { url: '#/create-cluster', id: 'cd-create-cluster' }
|
||||
};
|
||||
|
||||
createCluster() {
|
||||
cy.get('cd-create-cluster').should('contain.text', 'Welcome to Ceph');
|
||||
cy.get('[name=create-cluster]').click();
|
||||
}
|
||||
|
||||
doSkip() {
|
||||
cy.get('[name=skip-cluster-creation]').click();
|
||||
|
||||
cy.get('cd-dashboard').should('exist');
|
||||
const notification = new NotificationSidebarPageHelper();
|
||||
notification.open();
|
||||
notification.getNotifications().should('contain', 'Cluster creation skipped by user');
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
import { PageHelper } from '../page-helper.po';
|
||||
|
||||
export class CreateClusterReviewPageHelper extends PageHelper {
|
||||
pages = {
|
||||
index: { url: '#/create-cluster', id: 'cd-create-cluster-review' }
|
||||
};
|
||||
|
||||
checkDefaultHostName() {
|
||||
this.getTableCell(1, 'ceph-node-00.cephlab.com').should('exist');
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
import { CreateClusterWelcomePageHelper } from '../cluster/cluster-welcome-page.po';
|
||||
|
||||
describe('Create cluster page', () => {
|
||||
const createCluster = new CreateClusterWelcomePageHelper();
|
||||
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
Cypress.Cookies.preserveOnce('token');
|
||||
createCluster.navigateTo();
|
||||
});
|
||||
|
||||
it('should fail to create cluster', () => {
|
||||
createCluster.createCluster();
|
||||
});
|
||||
|
||||
it('should skip to dashboard landing page', () => {
|
||||
createCluster.doSkip();
|
||||
});
|
||||
});
|
@ -0,0 +1,61 @@
|
||||
import { CreateClusterWelcomePageHelper } from 'cypress/integration/cluster/cluster-welcome-page.po';
|
||||
import { CreateClusterReviewPageHelper } from 'cypress/integration/cluster/create-cluster-review.po';
|
||||
|
||||
describe('Create Cluster Review page', () => {
|
||||
const reviewPage = new CreateClusterReviewPageHelper();
|
||||
const createCluster = new CreateClusterWelcomePageHelper();
|
||||
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
Cypress.Cookies.preserveOnce('token');
|
||||
createCluster.navigateTo();
|
||||
createCluster.createCluster();
|
||||
|
||||
cy.get('button[aria-label="Next"]').click();
|
||||
});
|
||||
|
||||
describe('navigation link and title test', () => {
|
||||
it('should check if nav-link and title contains Review', () => {
|
||||
cy.get('.nav-link').should('contain.text', 'Review');
|
||||
|
||||
cy.get('.title').should('contain.text', 'Review');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fields check', () => {
|
||||
it('should check cluster resources table is present', () => {
|
||||
// check for table header 'Status'
|
||||
reviewPage.getLegends().its(0).should('have.text', 'Cluster Resources');
|
||||
|
||||
// check for fields in table
|
||||
reviewPage.getStatusTables().should('contain.text', 'Hosts');
|
||||
});
|
||||
|
||||
it('should check Hosts Per Label and Host Details tables are present', () => {
|
||||
// check for there to be two tables
|
||||
reviewPage.getDataTables().should('have.length', 2);
|
||||
|
||||
// check for table header 'Hosts Per Label'
|
||||
reviewPage.getLegends().its(1).should('have.text', 'Hosts Per Label');
|
||||
|
||||
// check for table header 'Host Details'
|
||||
reviewPage.getLegends().its(2).should('have.text', 'Host Details');
|
||||
|
||||
// verify correct columns on Hosts Per Label table
|
||||
reviewPage.getDataTableHeaders(0).contains('Label');
|
||||
|
||||
reviewPage.getDataTableHeaders(0).contains('Number of Hosts');
|
||||
|
||||
// verify correct columns on Host Details table
|
||||
reviewPage.getDataTableHeaders(1).contains('Host Name');
|
||||
|
||||
reviewPage.getDataTableHeaders(1).contains('Labels');
|
||||
});
|
||||
|
||||
it('should check hosts count and default host name are present', () => {
|
||||
reviewPage.getStatusTables().should('contain.text', '1');
|
||||
|
||||
reviewPage.checkDefaultHostName();
|
||||
});
|
||||
});
|
||||
});
|
@ -6,6 +6,7 @@ import _ from 'lodash';
|
||||
import { CephfsListComponent } from './ceph/cephfs/cephfs-list/cephfs-list.component';
|
||||
import { ConfigurationFormComponent } from './ceph/cluster/configuration/configuration-form/configuration-form.component';
|
||||
import { ConfigurationComponent } from './ceph/cluster/configuration/configuration.component';
|
||||
import { CreateClusterComponent } from './ceph/cluster/create-cluster/create-cluster.component';
|
||||
import { CrushmapComponent } from './ceph/cluster/crushmap/crushmap.component';
|
||||
import { HostFormComponent } from './ceph/cluster/hosts/host-form/host-form.component';
|
||||
import { HostsComponent } from './ceph/cluster/hosts/hosts.component';
|
||||
@ -89,6 +90,19 @@ const routes: Routes = [
|
||||
{ path: 'error', component: ErrorComponent },
|
||||
|
||||
// Cluster
|
||||
{
|
||||
path: 'create-cluster',
|
||||
component: CreateClusterComponent,
|
||||
canActivate: [ModuleStatusGuardService],
|
||||
data: {
|
||||
moduleStatusGuardConfig: {
|
||||
apiPath: 'orchestrator',
|
||||
redirectTo: 'dashboard',
|
||||
backend: 'cephadm'
|
||||
},
|
||||
breadcrumbs: 'Create Cluster'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'hosts',
|
||||
data: { breadcrumbs: 'Cluster/Hosts' },
|
||||
|
@ -21,6 +21,7 @@ import { CephSharedModule } from '../shared/ceph-shared.module';
|
||||
import { ConfigurationDetailsComponent } from './configuration/configuration-details/configuration-details.component';
|
||||
import { ConfigurationFormComponent } from './configuration/configuration-form/configuration-form.component';
|
||||
import { ConfigurationComponent } from './configuration/configuration.component';
|
||||
import { CreateClusterComponent } from './create-cluster/create-cluster.component';
|
||||
import { CrushmapComponent } from './crushmap/crushmap.component';
|
||||
import { HostDetailsComponent } from './hosts/host-details/host-details.component';
|
||||
import { HostFormComponent } from './hosts/host-form/host-form.component';
|
||||
@ -112,7 +113,8 @@ import { TelemetryComponent } from './telemetry/telemetry.component';
|
||||
PrometheusTabsComponent,
|
||||
ServiceFormComponent,
|
||||
OsdFlagsIndivModalComponent,
|
||||
PlacementPipe
|
||||
PlacementPipe,
|
||||
CreateClusterComponent
|
||||
]
|
||||
})
|
||||
export class ClusterModule {}
|
||||
|
@ -0,0 +1,29 @@
|
||||
<div class="container h-75">
|
||||
<div class="row h-100 justify-content-center align-items-center">
|
||||
<div class="blank-page">
|
||||
<!-- htmllint img-req-src="false" -->
|
||||
<img [src]="projectConstants.cephLogo"
|
||||
alt="Ceph"
|
||||
class="img-fluid mx-auto d-block">
|
||||
<h3 class="text-center m-2"
|
||||
i18n>Welcome to {{ projectConstants.projectName }}</h3>
|
||||
|
||||
<div class="m-4">
|
||||
<h4 class="text-center"
|
||||
i18n>Please proceed to complete the cluster creation</h4>
|
||||
<div class="offset-md-3">
|
||||
<button class="btn btn-accent m-3"
|
||||
name="create-cluster"
|
||||
[routerLink]="'/dashboard'"
|
||||
(click)="createCluster()"
|
||||
i18n>Create Cluster</button>
|
||||
<button class="btn btn-light"
|
||||
name="skip-cluster-creation"
|
||||
[routerLink]="'/dashboard'"
|
||||
(click)="skipClusterCreation()"
|
||||
i18n>Skip</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,45 @@
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
|
||||
import { ToastrModule } from 'ngx-toastr';
|
||||
|
||||
import { ClusterService } from '~/app/shared/api/cluster.service';
|
||||
import { SharedModule } from '~/app/shared/shared.module';
|
||||
import { configureTestBed } from '~/testing/unit-test-helper';
|
||||
import { CreateClusterComponent } from './create-cluster.component';
|
||||
|
||||
describe('CreateClusterComponent', () => {
|
||||
let component: CreateClusterComponent;
|
||||
let fixture: ComponentFixture<CreateClusterComponent>;
|
||||
let clusterService: ClusterService;
|
||||
|
||||
configureTestBed({
|
||||
declarations: [CreateClusterComponent],
|
||||
imports: [HttpClientTestingModule, RouterTestingModule, ToastrModule.forRoot(), SharedModule]
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CreateClusterComponent);
|
||||
component = fixture.componentInstance;
|
||||
clusterService = TestBed.inject(ClusterService);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have the heading "Welcome to Ceph Dashboard"', () => {
|
||||
const heading = fixture.debugElement.query(By.css('h3')).nativeElement;
|
||||
expect(heading.innerHTML).toBe('Welcome to Ceph Dashboard');
|
||||
});
|
||||
|
||||
it('should call updateStatus when cluster creation is skipped', () => {
|
||||
const clusterServiceSpy = spyOn(clusterService, 'updateStatus').and.callThrough();
|
||||
expect(clusterServiceSpy).not.toHaveBeenCalled();
|
||||
component.skipClusterCreation();
|
||||
expect(clusterServiceSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
@ -0,0 +1,44 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { ClusterService } from '~/app/shared/api/cluster.service';
|
||||
import { AppConstants } from '~/app/shared/constants/app.constants';
|
||||
import { NotificationType } from '~/app/shared/enum/notification-type.enum';
|
||||
import { Permission } from '~/app/shared/models/permissions';
|
||||
import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
|
||||
import { NotificationService } from '~/app/shared/services/notification.service';
|
||||
|
||||
@Component({
|
||||
selector: 'cd-create-cluster',
|
||||
templateUrl: './create-cluster.component.html',
|
||||
styleUrls: ['./create-cluster.component.scss']
|
||||
})
|
||||
export class CreateClusterComponent {
|
||||
permission: Permission;
|
||||
orchStatus = false;
|
||||
featureAvailable = false;
|
||||
projectConstants: typeof AppConstants = AppConstants;
|
||||
|
||||
constructor(
|
||||
private authStorageService: AuthStorageService,
|
||||
private clusterService: ClusterService,
|
||||
private notificationService: NotificationService
|
||||
) {
|
||||
this.permission = this.authStorageService.getPermissions().configOpt;
|
||||
}
|
||||
|
||||
createCluster() {
|
||||
this.notificationService.show(
|
||||
NotificationType.error,
|
||||
$localize`Cluster creation feature not implemented`
|
||||
);
|
||||
}
|
||||
|
||||
skipClusterCreation() {
|
||||
this.clusterService.updateStatus('POST_INSTALLED').subscribe(() => {
|
||||
this.notificationService.show(
|
||||
NotificationType.info,
|
||||
$localize`Cluster creation skipped by user`
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
@ -1,7 +1,11 @@
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Router } from '@angular/router';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { AuthService } from '~/app/shared/api/auth.service';
|
||||
import { configureTestBed } from '~/testing/unit-test-helper';
|
||||
import { AuthModule } from '../auth.module';
|
||||
import { LoginComponent } from './login.component';
|
||||
@ -9,6 +13,8 @@ import { LoginComponent } from './login.component';
|
||||
describe('LoginComponent', () => {
|
||||
let component: LoginComponent;
|
||||
let fixture: ComponentFixture<LoginComponent>;
|
||||
let routerNavigateSpy: jasmine.Spy;
|
||||
let authServiceLoginSpy: jasmine.Spy;
|
||||
|
||||
configureTestBed({
|
||||
imports: [RouterTestingModule, HttpClientTestingModule, AuthModule]
|
||||
@ -17,6 +23,10 @@ describe('LoginComponent', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LoginComponent);
|
||||
component = fixture.componentInstance;
|
||||
routerNavigateSpy = spyOn(TestBed.inject(Router), 'navigate');
|
||||
routerNavigateSpy.and.returnValue(true);
|
||||
authServiceLoginSpy = spyOn(TestBed.inject(AuthService), 'login');
|
||||
authServiceLoginSpy.and.returnValue(of(null));
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
@ -29,4 +39,20 @@ describe('LoginComponent', () => {
|
||||
component.ngOnInit();
|
||||
expect(component['modalService'].hasOpenModals()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should not show create cluster wizard if cluster creation was successful', () => {
|
||||
component.postInstalled = true;
|
||||
component.login();
|
||||
|
||||
expect(routerNavigateSpy).toHaveBeenCalledTimes(1);
|
||||
expect(routerNavigateSpy).toHaveBeenCalledWith(['/']);
|
||||
});
|
||||
|
||||
it('should show create cluster wizard if cluster creation was failed', () => {
|
||||
component.postInstalled = false;
|
||||
component.login();
|
||||
|
||||
expect(routerNavigateSpy).toHaveBeenCalledTimes(1);
|
||||
expect(routerNavigateSpy).toHaveBeenCalledWith(['/create-cluster']);
|
||||
});
|
||||
});
|
||||
|
@ -17,6 +17,7 @@ export class LoginComponent implements OnInit {
|
||||
model = new Credentials();
|
||||
isLoginActive = false;
|
||||
returnUrl: string;
|
||||
postInstalled = false;
|
||||
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
@ -43,6 +44,7 @@ export class LoginComponent implements OnInit {
|
||||
}
|
||||
this.authService.check(token).subscribe((login: any) => {
|
||||
if (login.login_url) {
|
||||
this.postInstalled = login.cluster_status === 'POST_INSTALLED';
|
||||
if (login.login_url === '#/login') {
|
||||
this.isLoginActive = true;
|
||||
} else {
|
||||
@ -63,7 +65,11 @@ export class LoginComponent implements OnInit {
|
||||
|
||||
login() {
|
||||
this.authService.login(this.model).subscribe(() => {
|
||||
const url = _.get(this.route.snapshot.queryParams, 'returnUrl', '/');
|
||||
const urlPath = this.postInstalled ? '/' : '/create-cluster';
|
||||
let url = _.get(this.route.snapshot.queryParams, 'returnUrl', urlPath);
|
||||
if (!this.postInstalled && this.route.snapshot.queryParams['returnUrl'] === '/dashboard') {
|
||||
url = '/create-cluster';
|
||||
}
|
||||
this.router.navigate([url]);
|
||||
});
|
||||
}
|
||||
|
@ -0,0 +1,42 @@
|
||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||
import { fakeAsync, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { configureTestBed } from '~/testing/unit-test-helper';
|
||||
import { ClusterService } from './cluster.service';
|
||||
|
||||
describe('ClusterService', () => {
|
||||
let service: ClusterService;
|
||||
let httpTesting: HttpTestingController;
|
||||
|
||||
configureTestBed({
|
||||
imports: [HttpClientTestingModule],
|
||||
providers: [ClusterService]
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(ClusterService);
|
||||
httpTesting = TestBed.inject(HttpTestingController);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
httpTesting.verify();
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should call getStatus', () => {
|
||||
service.getStatus().subscribe();
|
||||
const req = httpTesting.expectOne('api/cluster');
|
||||
expect(req.request.method).toBe('GET');
|
||||
});
|
||||
|
||||
it('should update cluster status', fakeAsync(() => {
|
||||
service.updateStatus('fakeStatus').subscribe();
|
||||
const req = httpTesting.expectOne('api/cluster');
|
||||
expect(req.request.method).toBe('PUT');
|
||||
expect(req.request.body).toEqual({ status: 'fakeStatus' });
|
||||
}));
|
||||
});
|
@ -0,0 +1,27 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ClusterService {
|
||||
baseURL = 'api/cluster';
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
getStatus(): Observable<string> {
|
||||
return this.http.get<string>(`${this.baseURL}`, {
|
||||
headers: { Accept: 'application/vnd.ceph.api.v0.1+json' }
|
||||
});
|
||||
}
|
||||
|
||||
updateStatus(status: string) {
|
||||
return this.http.put(
|
||||
`${this.baseURL}`,
|
||||
{ status: status },
|
||||
{ headers: { Accept: 'application/vnd.ceph.api.v0.1+json' } }
|
||||
);
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ export class AppConstants {
|
||||
public static readonly projectName = 'Ceph Dashboard';
|
||||
public static readonly license = 'Free software (LGPL 2.1).';
|
||||
public static readonly copyright = 'Copyright(c) ' + environment.year + ' Ceph contributors.';
|
||||
public static readonly cephLogo = 'assets/Ceph_Logo.svg';
|
||||
}
|
||||
|
||||
export enum URLVerbs {
|
||||
|
@ -7,6 +7,7 @@ import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { configureTestBed } from '~/testing/unit-test-helper';
|
||||
import { MgrModuleService } from '../api/mgr-module.service';
|
||||
import { ModuleStatusGuardService } from './module-status-guard.service';
|
||||
|
||||
describe('ModuleStatusGuardService', () => {
|
||||
@ -15,6 +16,7 @@ describe('ModuleStatusGuardService', () => {
|
||||
let router: Router;
|
||||
let route: ActivatedRouteSnapshot;
|
||||
let ngZone: NgZone;
|
||||
let mgrModuleService: MgrModuleService;
|
||||
|
||||
@Component({ selector: 'cd-foo', template: '' })
|
||||
class FooComponent {}
|
||||
@ -25,9 +27,16 @@ describe('ModuleStatusGuardService', () => {
|
||||
|
||||
const routes: Routes = [{ path: '**', component: FooComponent }];
|
||||
|
||||
const testCanActivate = (getResult: {}, activateResult: boolean, urlResult: string) => {
|
||||
const testCanActivate = (
|
||||
getResult: {},
|
||||
activateResult: boolean,
|
||||
urlResult: string,
|
||||
backend = 'cephadm'
|
||||
) => {
|
||||
let result: boolean;
|
||||
spyOn(httpClient, 'get').and.returnValue(observableOf(getResult));
|
||||
const test = { orchestrator: backend };
|
||||
spyOn(mgrModuleService, 'getConfig').and.returnValue(observableOf(test));
|
||||
ngZone.run(() => {
|
||||
service.canActivateChild(route).subscribe((resp) => {
|
||||
result = resp;
|
||||
@ -48,13 +57,15 @@ describe('ModuleStatusGuardService', () => {
|
||||
beforeEach(() => {
|
||||
service = TestBed.inject(ModuleStatusGuardService);
|
||||
httpClient = TestBed.inject(HttpClient);
|
||||
mgrModuleService = TestBed.inject(MgrModuleService);
|
||||
router = TestBed.inject(Router);
|
||||
route = new ActivatedRouteSnapshot();
|
||||
route.url = [];
|
||||
route.data = {
|
||||
moduleStatusGuardConfig: {
|
||||
apiPath: 'bar',
|
||||
redirectTo: '/foo'
|
||||
redirectTo: '/foo',
|
||||
backend: 'rook'
|
||||
}
|
||||
};
|
||||
ngZone = TestBed.inject(NgZone);
|
||||
@ -76,4 +87,8 @@ describe('ModuleStatusGuardService', () => {
|
||||
it('should test canActivateChild with status unavailable', fakeAsync(() => {
|
||||
testCanActivate(null, false, '/foo');
|
||||
}));
|
||||
|
||||
it('should redirect normally if the backend provided matches the current backend', fakeAsync(() => {
|
||||
testCanActivate({ available: true, message: 'foo' }, true, '/', 'rook');
|
||||
}));
|
||||
});
|
||||
|
@ -5,7 +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';
|
||||
import { MgrModuleService } from '~/app/shared/api/mgr-module.service';
|
||||
import { Icons } from '~/app/shared/enum/icons.enum';
|
||||
|
||||
/**
|
||||
* This service checks if a route can be activated by executing a
|
||||
@ -39,7 +40,11 @@ export class ModuleStatusGuardService implements CanActivate, CanActivateChild {
|
||||
// TODO: Hotfix - remove ALLOWLIST'ing when a generic ErrorComponent is implemented
|
||||
static readonly ALLOWLIST: string[] = ['501'];
|
||||
|
||||
constructor(private http: HttpClient, private router: Router) {}
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private router: Router,
|
||||
private mgrModuleService: MgrModuleService
|
||||
) {}
|
||||
|
||||
canActivate(route: ActivatedRouteSnapshot) {
|
||||
return this.doCheck(route);
|
||||
@ -54,9 +59,15 @@ export class ModuleStatusGuardService implements CanActivate, CanActivateChild {
|
||||
return observableOf(true);
|
||||
}
|
||||
const config = route.data['moduleStatusGuardConfig'];
|
||||
let backendCheck = false;
|
||||
if (config.backend) {
|
||||
this.mgrModuleService.getConfig('orchestrator').subscribe((resp) => {
|
||||
backendCheck = config.backend === resp['orchestrator'];
|
||||
});
|
||||
}
|
||||
return this.http.get(`api/${config.apiPath}/status`).pipe(
|
||||
map((resp: any) => {
|
||||
if (!resp.available) {
|
||||
if (!resp.available && !backendCheck) {
|
||||
this.router.navigate([config.redirectTo || ''], {
|
||||
state: {
|
||||
header: config.header,
|
||||
|
@ -2038,6 +2038,67 @@ paths:
|
||||
- jwt: []
|
||||
tags:
|
||||
- Cephfs
|
||||
/api/cluster:
|
||||
get:
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/vnd.ceph.api.v0.1+json:
|
||||
type: object
|
||||
description: OK
|
||||
'400':
|
||||
description: Operation exception. Please check the response body for details.
|
||||
'401':
|
||||
description: Unauthenticated access. Please login first.
|
||||
'403':
|
||||
description: Unauthorized access. Please check your permissions.
|
||||
'500':
|
||||
description: Unexpected error. Please check the response body for the stack
|
||||
trace.
|
||||
security:
|
||||
- jwt: []
|
||||
summary: Get the cluster status
|
||||
tags:
|
||||
- Cluster
|
||||
put:
|
||||
parameters: []
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
status:
|
||||
description: Cluster Status
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
type: object
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/vnd.ceph.api.v0.1+json:
|
||||
type: object
|
||||
description: Resource updated.
|
||||
'202':
|
||||
content:
|
||||
application/vnd.ceph.api.v0.1+json:
|
||||
type: object
|
||||
description: Operation is still executing. Please check the task queue.
|
||||
'400':
|
||||
description: Operation exception. Please check the response body for details.
|
||||
'401':
|
||||
description: Unauthenticated access. Please login first.
|
||||
'403':
|
||||
description: Unauthorized access. Please check your permissions.
|
||||
'500':
|
||||
description: Unexpected error. Please check the response body for the stack
|
||||
trace.
|
||||
security:
|
||||
- jwt: []
|
||||
summary: Update the cluster status
|
||||
tags:
|
||||
- Cluster
|
||||
/api/cluster_conf:
|
||||
get:
|
||||
parameters: []
|
||||
@ -10388,6 +10449,8 @@ tags:
|
||||
name: Auth
|
||||
- description: Cephfs Management API
|
||||
name: Cephfs
|
||||
- description: Get Cluster Details
|
||||
name: Cluster
|
||||
- description: Manage Cluster Configurations
|
||||
name: ClusterConfiguration
|
||||
- description: Crush Rule Management API
|
||||
|
26
src/pybind/mgr/dashboard/services/cluster.py
Normal file
26
src/pybind/mgr/dashboard/services/cluster.py
Normal file
@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from enum import Enum
|
||||
|
||||
from .. import mgr
|
||||
|
||||
|
||||
class ClusterModel:
|
||||
|
||||
class Status(Enum):
|
||||
INSTALLED = 0
|
||||
POST_INSTALLED = 1
|
||||
|
||||
status: Status
|
||||
|
||||
def __init__(self, status=Status.INSTALLED.name):
|
||||
self.status = self.Status[status]
|
||||
|
||||
def dict(self):
|
||||
return {'status': self.status.name}
|
||||
|
||||
def to_db(self):
|
||||
mgr.set_store('cluster/status', self.status.name)
|
||||
|
||||
@classmethod
|
||||
def from_db(cls):
|
||||
return cls(status=mgr.get_store('cluster/status', cls.Status.INSTALLED.name))
|
Loading…
Reference in New Issue
Block a user