mirror of
https://github.com/ceph/ceph
synced 2025-04-11 04:02:04 +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)
|
self.assertStatus(200)
|
||||||
data = self.jsonBody()
|
data = self.jsonBody()
|
||||||
self.assertSchema(data, JObj(sub_elems={
|
self.assertSchema(data, JObj(sub_elems={
|
||||||
"login_url": JLeaf(str)
|
"login_url": JLeaf(str),
|
||||||
|
"cluster_status": JLeaf(str)
|
||||||
}, allow_unknown=False))
|
}, allow_unknown=False))
|
||||||
self.logout()
|
self.logout()
|
||||||
|
|
||||||
@ -345,6 +346,7 @@ class AuthTest(DashboardTestCase):
|
|||||||
self.assertStatus(200)
|
self.assertStatus(200)
|
||||||
data = self.jsonBody()
|
data = self.jsonBody()
|
||||||
self.assertSchema(data, JObj(sub_elems={
|
self.assertSchema(data, JObj(sub_elems={
|
||||||
"login_url": JLeaf(str)
|
"login_url": JLeaf(str),
|
||||||
|
"cluster_status": JLeaf(str)
|
||||||
}, allow_unknown=False))
|
}, allow_unknown=False))
|
||||||
self.logout(set_cookies=True)
|
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 .. import mgr
|
||||||
from ..exceptions import InvalidCredentialsError, UserDoesNotExist
|
from ..exceptions import InvalidCredentialsError, UserDoesNotExist
|
||||||
from ..services.auth import AuthManager, JwtManager
|
from ..services.auth import AuthManager, JwtManager
|
||||||
|
from ..services.cluster import ClusterModel
|
||||||
from ..settings import Settings
|
from ..settings import Settings
|
||||||
from . import APIDoc, APIRouter, ControllerAuthMixin, EndpointDoc, RESTController, allow_empty_body
|
from . import APIDoc, APIRouter, ControllerAuthMixin, EndpointDoc, RESTController, allow_empty_body
|
||||||
|
|
||||||
@ -117,4 +118,5 @@ class Auth(RESTController, ControllerAuthMixin):
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
'login_url': self._get_login_url(),
|
'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 { CephfsListComponent } from './ceph/cephfs/cephfs-list/cephfs-list.component';
|
||||||
import { ConfigurationFormComponent } from './ceph/cluster/configuration/configuration-form/configuration-form.component';
|
import { ConfigurationFormComponent } from './ceph/cluster/configuration/configuration-form/configuration-form.component';
|
||||||
import { ConfigurationComponent } from './ceph/cluster/configuration/configuration.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 { CrushmapComponent } from './ceph/cluster/crushmap/crushmap.component';
|
||||||
import { HostFormComponent } from './ceph/cluster/hosts/host-form/host-form.component';
|
import { HostFormComponent } from './ceph/cluster/hosts/host-form/host-form.component';
|
||||||
import { HostsComponent } from './ceph/cluster/hosts/hosts.component';
|
import { HostsComponent } from './ceph/cluster/hosts/hosts.component';
|
||||||
@ -89,6 +90,19 @@ const routes: Routes = [
|
|||||||
{ path: 'error', component: ErrorComponent },
|
{ path: 'error', component: ErrorComponent },
|
||||||
|
|
||||||
// Cluster
|
// Cluster
|
||||||
|
{
|
||||||
|
path: 'create-cluster',
|
||||||
|
component: CreateClusterComponent,
|
||||||
|
canActivate: [ModuleStatusGuardService],
|
||||||
|
data: {
|
||||||
|
moduleStatusGuardConfig: {
|
||||||
|
apiPath: 'orchestrator',
|
||||||
|
redirectTo: 'dashboard',
|
||||||
|
backend: 'cephadm'
|
||||||
|
},
|
||||||
|
breadcrumbs: 'Create Cluster'
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'hosts',
|
path: 'hosts',
|
||||||
data: { breadcrumbs: 'Cluster/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 { ConfigurationDetailsComponent } from './configuration/configuration-details/configuration-details.component';
|
||||||
import { ConfigurationFormComponent } from './configuration/configuration-form/configuration-form.component';
|
import { ConfigurationFormComponent } from './configuration/configuration-form/configuration-form.component';
|
||||||
import { ConfigurationComponent } from './configuration/configuration.component';
|
import { ConfigurationComponent } from './configuration/configuration.component';
|
||||||
|
import { CreateClusterComponent } from './create-cluster/create-cluster.component';
|
||||||
import { CrushmapComponent } from './crushmap/crushmap.component';
|
import { CrushmapComponent } from './crushmap/crushmap.component';
|
||||||
import { HostDetailsComponent } from './hosts/host-details/host-details.component';
|
import { HostDetailsComponent } from './hosts/host-details/host-details.component';
|
||||||
import { HostFormComponent } from './hosts/host-form/host-form.component';
|
import { HostFormComponent } from './hosts/host-form/host-form.component';
|
||||||
@ -112,7 +113,8 @@ import { TelemetryComponent } from './telemetry/telemetry.component';
|
|||||||
PrometheusTabsComponent,
|
PrometheusTabsComponent,
|
||||||
ServiceFormComponent,
|
ServiceFormComponent,
|
||||||
OsdFlagsIndivModalComponent,
|
OsdFlagsIndivModalComponent,
|
||||||
PlacementPipe
|
PlacementPipe,
|
||||||
|
CreateClusterComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class ClusterModule {}
|
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 { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
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 { configureTestBed } from '~/testing/unit-test-helper';
|
||||||
import { AuthModule } from '../auth.module';
|
import { AuthModule } from '../auth.module';
|
||||||
import { LoginComponent } from './login.component';
|
import { LoginComponent } from './login.component';
|
||||||
@ -9,6 +13,8 @@ import { LoginComponent } from './login.component';
|
|||||||
describe('LoginComponent', () => {
|
describe('LoginComponent', () => {
|
||||||
let component: LoginComponent;
|
let component: LoginComponent;
|
||||||
let fixture: ComponentFixture<LoginComponent>;
|
let fixture: ComponentFixture<LoginComponent>;
|
||||||
|
let routerNavigateSpy: jasmine.Spy;
|
||||||
|
let authServiceLoginSpy: jasmine.Spy;
|
||||||
|
|
||||||
configureTestBed({
|
configureTestBed({
|
||||||
imports: [RouterTestingModule, HttpClientTestingModule, AuthModule]
|
imports: [RouterTestingModule, HttpClientTestingModule, AuthModule]
|
||||||
@ -17,6 +23,10 @@ describe('LoginComponent', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(LoginComponent);
|
fixture = TestBed.createComponent(LoginComponent);
|
||||||
component = fixture.componentInstance;
|
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();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -29,4 +39,20 @@ describe('LoginComponent', () => {
|
|||||||
component.ngOnInit();
|
component.ngOnInit();
|
||||||
expect(component['modalService'].hasOpenModals()).toBeFalsy();
|
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();
|
model = new Credentials();
|
||||||
isLoginActive = false;
|
isLoginActive = false;
|
||||||
returnUrl: string;
|
returnUrl: string;
|
||||||
|
postInstalled = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
@ -43,6 +44,7 @@ export class LoginComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
this.authService.check(token).subscribe((login: any) => {
|
this.authService.check(token).subscribe((login: any) => {
|
||||||
if (login.login_url) {
|
if (login.login_url) {
|
||||||
|
this.postInstalled = login.cluster_status === 'POST_INSTALLED';
|
||||||
if (login.login_url === '#/login') {
|
if (login.login_url === '#/login') {
|
||||||
this.isLoginActive = true;
|
this.isLoginActive = true;
|
||||||
} else {
|
} else {
|
||||||
@ -63,7 +65,11 @@ export class LoginComponent implements OnInit {
|
|||||||
|
|
||||||
login() {
|
login() {
|
||||||
this.authService.login(this.model).subscribe(() => {
|
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]);
|
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 projectName = 'Ceph Dashboard';
|
||||||
public static readonly license = 'Free software (LGPL 2.1).';
|
public static readonly license = 'Free software (LGPL 2.1).';
|
||||||
public static readonly copyright = 'Copyright(c) ' + environment.year + ' Ceph contributors.';
|
public static readonly copyright = 'Copyright(c) ' + environment.year + ' Ceph contributors.';
|
||||||
|
public static readonly cephLogo = 'assets/Ceph_Logo.svg';
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum URLVerbs {
|
export enum URLVerbs {
|
||||||
|
@ -7,6 +7,7 @@ import { RouterTestingModule } from '@angular/router/testing';
|
|||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
import { configureTestBed } from '~/testing/unit-test-helper';
|
import { configureTestBed } from '~/testing/unit-test-helper';
|
||||||
|
import { MgrModuleService } from '../api/mgr-module.service';
|
||||||
import { ModuleStatusGuardService } from './module-status-guard.service';
|
import { ModuleStatusGuardService } from './module-status-guard.service';
|
||||||
|
|
||||||
describe('ModuleStatusGuardService', () => {
|
describe('ModuleStatusGuardService', () => {
|
||||||
@ -15,6 +16,7 @@ describe('ModuleStatusGuardService', () => {
|
|||||||
let router: Router;
|
let router: Router;
|
||||||
let route: ActivatedRouteSnapshot;
|
let route: ActivatedRouteSnapshot;
|
||||||
let ngZone: NgZone;
|
let ngZone: NgZone;
|
||||||
|
let mgrModuleService: MgrModuleService;
|
||||||
|
|
||||||
@Component({ selector: 'cd-foo', template: '' })
|
@Component({ selector: 'cd-foo', template: '' })
|
||||||
class FooComponent {}
|
class FooComponent {}
|
||||||
@ -25,9 +27,16 @@ describe('ModuleStatusGuardService', () => {
|
|||||||
|
|
||||||
const routes: Routes = [{ path: '**', component: FooComponent }];
|
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;
|
let result: boolean;
|
||||||
spyOn(httpClient, 'get').and.returnValue(observableOf(getResult));
|
spyOn(httpClient, 'get').and.returnValue(observableOf(getResult));
|
||||||
|
const test = { orchestrator: backend };
|
||||||
|
spyOn(mgrModuleService, 'getConfig').and.returnValue(observableOf(test));
|
||||||
ngZone.run(() => {
|
ngZone.run(() => {
|
||||||
service.canActivateChild(route).subscribe((resp) => {
|
service.canActivateChild(route).subscribe((resp) => {
|
||||||
result = resp;
|
result = resp;
|
||||||
@ -48,13 +57,15 @@ describe('ModuleStatusGuardService', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
service = TestBed.inject(ModuleStatusGuardService);
|
service = TestBed.inject(ModuleStatusGuardService);
|
||||||
httpClient = TestBed.inject(HttpClient);
|
httpClient = TestBed.inject(HttpClient);
|
||||||
|
mgrModuleService = TestBed.inject(MgrModuleService);
|
||||||
router = TestBed.inject(Router);
|
router = TestBed.inject(Router);
|
||||||
route = new ActivatedRouteSnapshot();
|
route = new ActivatedRouteSnapshot();
|
||||||
route.url = [];
|
route.url = [];
|
||||||
route.data = {
|
route.data = {
|
||||||
moduleStatusGuardConfig: {
|
moduleStatusGuardConfig: {
|
||||||
apiPath: 'bar',
|
apiPath: 'bar',
|
||||||
redirectTo: '/foo'
|
redirectTo: '/foo',
|
||||||
|
backend: 'rook'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
ngZone = TestBed.inject(NgZone);
|
ngZone = TestBed.inject(NgZone);
|
||||||
@ -76,4 +87,8 @@ describe('ModuleStatusGuardService', () => {
|
|||||||
it('should test canActivateChild with status unavailable', fakeAsync(() => {
|
it('should test canActivateChild with status unavailable', fakeAsync(() => {
|
||||||
testCanActivate(null, false, '/foo');
|
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 { of as observableOf } from 'rxjs';
|
||||||
import { catchError, map } from 'rxjs/operators';
|
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
|
* 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
|
// TODO: Hotfix - remove ALLOWLIST'ing when a generic ErrorComponent is implemented
|
||||||
static readonly ALLOWLIST: string[] = ['501'];
|
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) {
|
canActivate(route: ActivatedRouteSnapshot) {
|
||||||
return this.doCheck(route);
|
return this.doCheck(route);
|
||||||
@ -54,9 +59,15 @@ export class ModuleStatusGuardService implements CanActivate, CanActivateChild {
|
|||||||
return observableOf(true);
|
return observableOf(true);
|
||||||
}
|
}
|
||||||
const config = route.data['moduleStatusGuardConfig'];
|
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(
|
return this.http.get(`api/${config.apiPath}/status`).pipe(
|
||||||
map((resp: any) => {
|
map((resp: any) => {
|
||||||
if (!resp.available) {
|
if (!resp.available && !backendCheck) {
|
||||||
this.router.navigate([config.redirectTo || ''], {
|
this.router.navigate([config.redirectTo || ''], {
|
||||||
state: {
|
state: {
|
||||||
header: config.header,
|
header: config.header,
|
||||||
|
@ -2038,6 +2038,67 @@ paths:
|
|||||||
- jwt: []
|
- jwt: []
|
||||||
tags:
|
tags:
|
||||||
- Cephfs
|
- 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:
|
/api/cluster_conf:
|
||||||
get:
|
get:
|
||||||
parameters: []
|
parameters: []
|
||||||
@ -10388,6 +10449,8 @@ tags:
|
|||||||
name: Auth
|
name: Auth
|
||||||
- description: Cephfs Management API
|
- description: Cephfs Management API
|
||||||
name: Cephfs
|
name: Cephfs
|
||||||
|
- description: Get Cluster Details
|
||||||
|
name: Cluster
|
||||||
- description: Manage Cluster Configurations
|
- description: Manage Cluster Configurations
|
||||||
name: ClusterConfiguration
|
name: ClusterConfiguration
|
||||||
- description: Crush Rule Management API
|
- 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