From: Devika Babrekar Date: Mon, 18 May 2026 14:23:22 +0000 (+0530) Subject: mgr/dashboard: Converting add storage wizard into tearsheet X-Git-Tag: v21.0.1~44^2 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=718e1fe5e651cd31ed1a830b001c2e4a82e10ba4;p=ceph.git mgr/dashboard: Converting add storage wizard into tearsheet Fixes: https://tracker.ceph.com/issues/76652 Signed-off-by: Devika Babrekar --- diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/create-cluster.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/create-cluster.po.ts index 00bd2763382..c0c78b4ceb3 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/create-cluster.po.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/create-cluster.po.ts @@ -9,7 +9,19 @@ export class OnboardingHelper extends PageHelper { onboarding() { cy.get('cd-create-cluster').should('contain.text', 'Welcome to Ceph Dashboard'); cy.get('[aria-label="Add Storage"]').first().click({ force: true }); - cy.get('cd-wizard').should('exist'); + cy.get('cd-tearsheet').should('exist'); + } + + selectStep(stepLabel: string) { + cy.get('cd-tearsheet cds-progress-indicator').contains(stepLabel).click(); + } + + clickNext() { + cy.get('cd-tearsheet').contains('button', 'Next').click(); + } + + submitStorage() { + cy.get('cd-tearsheet .tearsheet-footer-submit').click(); } doSkip() { diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/create-cluster/create-cluster.feature.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/create-cluster/create-cluster.feature.po.ts index b42ea14f7fa..947b0431f6e 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/create-cluster/create-cluster.feature.po.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/common/create-cluster/create-cluster.feature.po.ts @@ -1,10 +1,7 @@ import { Given, Then } from 'cypress-cucumber-preprocessor/steps'; Given('I am on the {string} section', (page: string) => { - cy.get('cd-wizard').within(() => { - cy.get('button').should('have.attr', 'title', page).first().click(); - cy.get('.cds--assistive-text').should('contain.text', 'Current'); - }); + cy.get('cd-tearsheet cds-progress-indicator').contains(page).click(); }); Then('I should see a message {string}', () => { diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/03-create-cluster-create-services.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/03-create-cluster-create-services.e2e-spec.ts index a380a0e5d97..9c96d2a19ed 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/03-create-cluster-create-services.e2e-spec.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/03-create-cluster-create-services.e2e-spec.ts @@ -17,9 +17,7 @@ describe('Create cluster create services page', () => { onboardingPage.navigateTo(); onboardingPage.onboarding(); - cy.get('cd-wizard').within(() => { - cy.get('button').contains('Create Services').click(); - }); + onboardingPage.selectStep('Create Services'); }); it('should check if title contains Create Services', () => { diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/04-create-cluster-create-osds.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/04-create-cluster-create-osds.e2e-spec.ts index d2c1e5b6769..541fff0bcb4 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/04-create-cluster-create-osds.e2e-spec.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/04-create-cluster-create-osds.e2e-spec.ts @@ -12,9 +12,7 @@ describe('Add storage - create osds page', () => { cy.login(); onboarding.navigateTo(); onboarding.onboarding(); - cy.get('cd-wizard').within(() => { - cy.get('button').contains('Create OSDs').click(); - }); + onboarding.selectStep('Create OSDs'); }); it('should check if title contains Create OSDs', () => { @@ -30,16 +28,12 @@ describe('Add storage - create osds page', () => { // Go to the Review section and Expand the cluster // because the drive group spec is only stored // in frontend and will be lost when refreshed - cy.get('cd-wizard').within(() => { - cy.get('button').contains('Review').click(); - }); - cy.get('button[aria-label="Next"]').click(); + onboarding.selectStep('Review'); + onboarding.submitStorage(); cy.get('cd-overview').should('exist'); onboarding.navigateTo(); onboarding.onboarding(); - cy.get('cd-wizard').within(() => { - cy.get('button').contains('Create OSDs').click(); - }); + onboarding.selectStep('Create OSDs'); } }); }); diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/05-create-cluster-review.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/05-create-cluster-review.e2e-spec.ts index ad9930b01e2..b81c388faab 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/05-create-cluster-review.e2e-spec.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/05-create-cluster-review.e2e-spec.ts @@ -11,9 +11,7 @@ describe('Create Cluster Review page', () => { onboarding.navigateTo(); onboarding.onboarding(); - cy.get('cd-wizard').within(() => { - cy.get('button').contains('Review').click(); - }); + onboarding.selectStep('Review'); }); describe('fields check', () => { diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/06-cluster-check.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/06-cluster-check.e2e-spec.ts index 35905ebd549..103a145e1e6 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/06-cluster-check.e2e-spec.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/06-cluster-check.e2e-spec.ts @@ -21,15 +21,11 @@ describe('when cluster creation is completed', () => { // Explicitly skip OSD Creation Step so that it prevents from // deploying OSDs to the hosts automatically. - cy.get('cd-wizard').within(() => { - cy.get('button').contains('Create OSDs').click(); - }); - cy.get('button[aria-label="Skip this step"]').click(); + onboarding.selectStep('Create OSDs'); + cy.get('#skipStepBtn').click(); - cy.get('cd-wizard').within(() => { - cy.get('button').contains('Review').click(); - }); - cy.get('button[aria-label="Next"]').click(); + onboarding.selectStep('Review'); + onboarding.submitStorage(); cy.get('cd-overview').should('exist'); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts index d636b5dc996..04c158cbc80 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts @@ -51,6 +51,10 @@ import { ConfigurationDetailsComponent } from './configuration/configuration-det import { ConfigurationFormComponent } from './configuration/configuration-form/configuration-form.component'; import { ConfigurationComponent } from './configuration/configuration.component'; import { CreateClusterReviewComponent } from './create-cluster/create-cluster-review.component'; +import { CreateClusterStep1Component } from './create-cluster/create-cluster-step-1/create-cluster-step-1.component'; +import { CreateClusterStep2Component } from './create-cluster/create-cluster-step-2/create-cluster-step-2.component'; +import { CreateClusterStep3Component } from './create-cluster/create-cluster-step-3/create-cluster-step-3.component'; +import { CreateClusterStep4Component } from './create-cluster/create-cluster-step-4/create-cluster-step-4.component'; import { CreateClusterComponent } from './create-cluster/create-cluster.component'; import { CrushmapComponent } from './crushmap/crushmap.component'; import { HostDetailsComponent } from './hosts/host-details/host-details.component'; @@ -178,6 +182,10 @@ import { TextLabelListComponent } from '~/app/shared/components/text-label-list/ PlacementPipe, CreateClusterComponent, CreateClusterReviewComponent, + CreateClusterStep1Component, + CreateClusterStep2Component, + CreateClusterStep3Component, + CreateClusterStep4Component, UpgradeComponent, UpgradeStartModalComponent, UpgradeProgressComponent, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.scss index beecca09671..1780a0cdc64 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.scss @@ -1,5 +1,7 @@ -cd-hosts { - ::ng-deep .nav { - display: none; - } +.nav { + display: none; +} + +.cds--row { + margin-inline: 0 !important; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.ts index e8c47e56ed6..ac6ec88eada 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, ViewEncapsulation } from '@angular/core'; import _ from 'lodash'; @@ -14,7 +14,8 @@ import { WizardStepsService } from '~/app/shared/services/wizard-steps.service'; selector: 'cd-create-cluster-review', templateUrl: './create-cluster-review.component.html', styleUrls: ['./create-cluster-review.component.scss'], - standalone: false + standalone: false, + encapsulation: ViewEncapsulation.None }) export class CreateClusterReviewComponent implements OnInit { hosts: object[] = []; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-1/create-cluster-step-1.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-1/create-cluster-step-1.component.html new file mode 100644 index 00000000000..23ff0df4df1 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-1/create-cluster-step-1.component.html @@ -0,0 +1,15 @@ +
+
+

Add Hosts

+ + +
+
\ No newline at end of file diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-1/create-cluster-step-1.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-1/create-cluster-step-1.component.scss new file mode 100644 index 00000000000..f5788bcc3b2 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-1/create-cluster-step-1.component.scss @@ -0,0 +1,3 @@ +.nav { + display: none; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-1/create-cluster-step-1.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-1/create-cluster-step-1.component.ts new file mode 100644 index 00000000000..32d35991346 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-1/create-cluster-step-1.component.ts @@ -0,0 +1,19 @@ +import { Component, OnInit, ViewEncapsulation } from '@angular/core'; + +import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; +import { TearsheetStep } from '~/app/shared/models/tearsheet-step'; + +@Component({ + selector: 'cd-create-cluster-step-1', + templateUrl: './create-cluster-step-1.component.html', + styleUrls: ['./create-cluster-step-1.component.scss'], + standalone: false, + encapsulation: ViewEncapsulation.None +}) +export class CreateClusterStep1Component implements OnInit, TearsheetStep { + formGroup: CdFormGroup; + + ngOnInit() { + this.formGroup = new CdFormGroup({}); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-2/create-cluster-step-2.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-2/create-cluster-step-2.component.html new file mode 100644 index 00000000000..c847a491e7d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-2/create-cluster-step-2.component.html @@ -0,0 +1,25 @@ +
+
+ +

Create OSDs

+ +
+ +
+ + +
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-2/create-cluster-step-2.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-2/create-cluster-step-2.component.scss new file mode 100644 index 00000000000..12250e4ec11 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-2/create-cluster-step-2.component.scss @@ -0,0 +1,9 @@ +cd-osd-form { + .card { + border: 0; + } + + .accordion { + margin-left: -1.5rem; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-2/create-cluster-step-2.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-2/create-cluster-step-2.component.ts new file mode 100644 index 00000000000..a144b3ac7fd --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-2/create-cluster-step-2.component.ts @@ -0,0 +1,45 @@ +import { Component, EventEmitter, OnInit, Output, ViewEncapsulation } from '@angular/core'; +import { UntypedFormControl } from '@angular/forms'; + +import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; +import { TearsheetStep } from '~/app/shared/models/tearsheet-step'; +import { DriveGroup } from '../../osd/osd-form/drive-group.model'; + +@Component({ + selector: 'cd-create-cluster-step-2', + templateUrl: './create-cluster-step-2.component.html', + styleUrls: ['./create-cluster-step-2.component.scss'], + standalone: false, + encapsulation: ViewEncapsulation.None +}) +export class CreateClusterStep2Component implements OnInit, TearsheetStep { + @Output() skipStep = new EventEmitter(); + + formGroup: CdFormGroup; + + ngOnInit() { + this.formGroup = new CdFormGroup({ + skipped: new UntypedFormControl(false), + driveGroup: new UntypedFormControl(null), + selectedOption: new UntypedFormControl(null), + simpleDeployment: new UntypedFormControl(true) + }); + } + + onSkip() { + this.formGroup.patchValue({ skipped: true }); + this.skipStep.emit(); + } + + setDriveGroup(driveGroup: DriveGroup) { + this.formGroup.patchValue({ driveGroup }); + } + + setDeploymentOptions(option: object) { + this.formGroup.patchValue({ selectedOption: option }); + } + + setDeploymentMode(mode: boolean) { + this.formGroup.patchValue({ simpleDeployment: mode }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-3/create-cluster-step-3.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-3/create-cluster-step-3.component.html new file mode 100644 index 00000000000..4beaf46b679 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-3/create-cluster-step-3.component.html @@ -0,0 +1,15 @@ +
+
+ +

Create Services

+ + +
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-3/create-cluster-step-3.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-3/create-cluster-step-3.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-3/create-cluster-step-3.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-3/create-cluster-step-3.component.ts new file mode 100644 index 00000000000..a747869ccc7 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-3/create-cluster-step-3.component.ts @@ -0,0 +1,18 @@ +import { Component, OnInit } from '@angular/core'; + +import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; +import { TearsheetStep } from '~/app/shared/models/tearsheet-step'; + +@Component({ + selector: 'cd-create-cluster-step-3', + templateUrl: './create-cluster-step-3.component.html', + styleUrls: ['./create-cluster-step-3.component.scss'], + standalone: false +}) +export class CreateClusterStep3Component implements OnInit, TearsheetStep { + formGroup: CdFormGroup; + + ngOnInit() { + this.formGroup = new CdFormGroup({}); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-4/create-cluster-step-4.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-4/create-cluster-step-4.component.html new file mode 100644 index 00000000000..984b16bf9f5 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-4/create-cluster-step-4.component.html @@ -0,0 +1,9 @@ +
+
+ +
+
\ No newline at end of file diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-4/create-cluster-step-4.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-4/create-cluster-step-4.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-4/create-cluster-step-4.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-4/create-cluster-step-4.component.ts new file mode 100644 index 00000000000..845c9751306 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-4/create-cluster-step-4.component.ts @@ -0,0 +1,18 @@ +import { Component, OnInit } from '@angular/core'; + +import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; +import { TearsheetStep } from '~/app/shared/models/tearsheet-step'; + +@Component({ + selector: 'cd-create-cluster-step-4', + templateUrl: './create-cluster-step-4.component.html', + styleUrls: ['./create-cluster-step-4.component.scss'], + standalone: false +}) +export class CreateClusterStep4Component implements OnInit, TearsheetStep { + formGroup: CdFormGroup; + + ngOnInit() { + this.formGroup = new CdFormGroup({}); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.html index 5c7687bb3a2..08dcae08f4f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.html @@ -1,5 +1,5 @@ @if (startClusterCreation) { -
+
-
- -
Add Storage
- -
- -
- -
-

Add Hosts

- - -
-
-

Create OSDs

-
- -
-
-
-

Create Services

- -
-
- -
-
-
- @if (stepTitles[currentStep?.stepIndex]?.label === 'Create OSDs') { - - } - - -
-
-
+ + + + + + + + + + + + + + } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.scss index 3fb39ceef78..0503283fe5e 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.scss @@ -1,30 +1,19 @@ @use '@carbon/layout'; .container-fluid { - align-items: flex-start; - display: flex; padding-left: 0; + padding-bottom: 0 !important; width: 100%; -} - -cd-hosts { - ::ng-deep .nav { - display: none; - } -} - -cd-osd-form { - ::ng-deep .card { - border: 0; - } - ::ng-deep .accordion { - margin-left: -1.5rem; + &-main { + align-items: flex-start; + display: flex; } } .storage-requirements-header { border-bottom: 0; + margin-top: var(--cds-spacing-10) !important; } .ceph-logo { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.spec.ts index 01e9c6aa2a9..7ee0b057494 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.spec.ts @@ -10,15 +10,15 @@ import { OsdService } from '~/app/shared/api/osd.service'; import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component'; import { AppConstants } from '~/app/shared/constants/app.constants'; import { ModalService } from '~/app/shared/services/modal.service'; -import { WizardStepsService } from '~/app/shared/services/wizard-steps.service'; import { SharedModule } from '~/app/shared/shared.module'; import { configureTestBed } from '~/testing/unit-test-helper'; import { CreateClusterComponent } from './create-cluster.component'; +import { CreateClusterStep2Component } from './create-cluster-step-2/create-cluster-step-2.component'; +import { CreateClusterStep3Component } from './create-cluster-step-3/create-cluster-step-3.component'; describe('CreateClusterComponent', () => { let component: CreateClusterComponent; let fixture: ComponentFixture; - let wizardStepService: WizardStepsService; let hostService: HostService; let osdService: OsdService; let modalServiceShowSpy: jasmine.Spy; @@ -29,18 +29,25 @@ describe('CreateClusterComponent', () => { }); beforeEach(() => { + TestBed.overrideComponent(CreateClusterStep3Component, { + set: { template: '
' } + }); + fixture = TestBed.createComponent(CreateClusterComponent); component = fixture.componentInstance; - wizardStepService = TestBed.inject(WizardStepsService); hostService = TestBed.inject(HostService); osdService = TestBed.inject(OsdService); modalServiceShowSpy = spyOn(TestBed.inject(ModalService), 'show').and.returnValue({ - // mock the close function, it might be called if there are async tests. close: jest.fn() }); fixture.detectChanges(); }); + const openTearsheet = () => { + component.createCluster(); + fixture.detectChanges(); + }; + it('should create', () => { expect(component).toBeTruthy(); }); @@ -61,67 +68,24 @@ describe('CreateClusterComponent', () => { expect(modalServiceShowSpy.calls.first().args[0]).toBe(ConfirmationModalComponent); }); - it('should show the wizard when cluster creation is started', () => { - component.createCluster(); - fixture.detectChanges(); + it('should show the tearsheet when cluster creation is started', () => { + openTearsheet(); const nativeEl = fixture.debugElement.nativeElement; - expect(nativeEl.querySelector('cd-wizard')).not.toBe(null); + expect(nativeEl.querySelector('cd-tearsheet')).not.toBe(null); }); - it('should have title Add Hosts', () => { - component.createCluster(); - fixture.detectChanges(); - const heading = fixture.debugElement.query(By.css('.title')).nativeElement; - expect(heading.innerHTML).toBe('Add Hosts'); + it('should have Add Hosts step component when cluster creation is started', () => { + openTearsheet(); + const nativeEl = fixture.debugElement.nativeElement; + expect(nativeEl.querySelector('cd-create-cluster-step-1')).not.toBe(null); }); - it('should show the host list when cluster creation as first step', () => { - component.createCluster(); - fixture.detectChanges(); + it('should show the host list in the first step', () => { + openTearsheet(); const nativeEl = fixture.debugElement.nativeElement; expect(nativeEl.querySelector('cd-hosts')).not.toBe(null); }); - it('should move to next step and show the second page', () => { - const wizardStepServiceSpy = spyOn(wizardStepService, 'moveToNextStep').and.callThrough(); - component.createCluster(); - fixture.detectChanges(); - component.onNextStep(); - fixture.detectChanges(); - expect(wizardStepServiceSpy).toHaveBeenCalledTimes(1); - }); - - it('should show the button labels correctly', () => { - component.createCluster(); - fixture.detectChanges(); - let submitBtnLabel = component.showSubmitButtonLabel(); - expect(submitBtnLabel).toEqual('Next'); - let cancelBtnLabel = component.showCancelButtonLabel(); - expect(cancelBtnLabel).toEqual('Cancel'); - - component.onNextStep(); - fixture.detectChanges(); - submitBtnLabel = component.showSubmitButtonLabel(); - expect(submitBtnLabel).toEqual('Next'); - cancelBtnLabel = component.showCancelButtonLabel(); - expect(cancelBtnLabel).toEqual('Back'); - - component.onNextStep(); - fixture.detectChanges(); - submitBtnLabel = component.showSubmitButtonLabel(); - expect(submitBtnLabel).toEqual('Next'); - cancelBtnLabel = component.showCancelButtonLabel(); - expect(cancelBtnLabel).toEqual('Back'); - - // Last page of the wizard - component.onNextStep(); - fixture.detectChanges(); - submitBtnLabel = component.showSubmitButtonLabel(); - expect(submitBtnLabel).toEqual('Add Storage'); - cancelBtnLabel = component.showCancelButtonLabel(); - expect(cancelBtnLabel).toEqual('Back'); - }); - it('should ensure osd creation did not happen when no devices are selected', () => { component.simpleDeployment = false; const osdServiceSpy = spyOn(osdService, 'create').and.callThrough(); @@ -144,27 +108,34 @@ describe('CreateClusterComponent', () => { expect(hostServiceSpy).toHaveBeenCalledTimes(1); }); - it('should show skip button in the Create OSDs Steps', () => { - component.createCluster(); - fixture.detectChanges(); + it('should fire cluster submit when tearsheet Add Storage is clicked on review step', () => { + const submitSpy = spyOn(component, 'onSubmit').and.callThrough(); + const hostServiceSpy = spyOn(hostService, 'list').and.callThrough(); - component.onNextStep(); + openTearsheet(); + component.onSubmit(); fixture.detectChanges(); - const skipBtn = fixture.debugElement.query(By.css('#skipStepBtn')).nativeElement; + + expect(submitSpy).toHaveBeenCalled(); + expect(hostServiceSpy).toHaveBeenCalled(); + }); + + it('should show skip button in the Create OSDs step', () => { + const stepFixture = TestBed.createComponent(CreateClusterStep2Component); + stepFixture.detectChanges(); + const skipBtn = stepFixture.debugElement.query(By.css('#skipStepBtn')).nativeElement; expect(skipBtn).not.toBe(null); expect(skipBtn.innerHTML).toBe('Skip'); }); - it('should skip the Create OSDs Steps', () => { - component.createCluster(); - fixture.detectChanges(); + it('should skip the Create OSDs step', () => { + openTearsheet(); + spyOn(component.tearsheet, 'onNext'); - component.onNextStep(); - fixture.detectChanges(); - const skipBtn = fixture.debugElement.query(By.css('#skipStepBtn')).nativeElement; - skipBtn.click(); + component.onSkipOsdStep(); fixture.detectChanges(); expect(component.stepsToSkip['Create OSDs']).toBe(true); + expect(component.tearsheet.onNext).toHaveBeenCalled(); }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.ts index 133c489901f..cff79b5d365 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.ts @@ -1,73 +1,70 @@ import { - AfterViewInit, - ChangeDetectorRef, Component, - EventEmitter, OnDestroy, OnInit, - Output, TemplateRef, - ViewChild + ViewChild, + ViewEncapsulation } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import _ from 'lodash'; -import { forkJoin, Subscription } from 'rxjs'; +import { forkJoin } from 'rxjs'; import { finalize } from 'rxjs/operators'; +import { Step } from 'carbon-components-angular'; import { ClusterService } from '~/app/shared/api/cluster.service'; import { HostService } from '~/app/shared/api/host.service'; import { OsdService } from '~/app/shared/api/osd.service'; import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component'; -import { ActionLabelsI18n, AppConstants, URLVerbs } from '~/app/shared/constants/app.constants'; +import { TearsheetComponent } from '~/app/shared/components/tearsheet/tearsheet.component'; +import { AppConstants, URLVerbs } from '~/app/shared/constants/app.constants'; import { NotificationType } from '~/app/shared/enum/notification-type.enum'; import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context'; import { FinishedTask } from '~/app/shared/models/finished-task'; import { DeploymentOptions } from '~/app/shared/models/osd-deployment-options'; import { Permissions } from '~/app/shared/models/permissions'; -import { WizardStepModel } from '~/app/shared/models/wizard-steps'; import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; import { NotificationService } from '~/app/shared/services/notification.service'; import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; -import { WizardStepsService } from '~/app/shared/services/wizard-steps.service'; -import { DriveGroup } from '../osd/osd-form/drive-group.model'; -import { Location } from '@angular/common'; import { ModalCdsService } from '~/app/shared/services/modal-cds.service'; -import { Step } from 'carbon-components-angular'; +import { DriveGroup } from '../osd/osd-form/drive-group.model'; import { Icons } from '~/app/shared/enum/icons.enum'; +const STEP_LABELS = { + ADD_HOSTS: $localize`Add Hosts`, + CREATE_OSDS: $localize`Create OSDs`, + CREATE_SERVICES: $localize`Create Services`, + REVIEW: $localize`Review` +} as const; + @Component({ selector: 'cd-create-cluster', templateUrl: './create-cluster.component.html', styleUrls: ['./create-cluster.component.scss'], - standalone: false + standalone: false, + encapsulation: ViewEncapsulation.None }) -export class CreateClusterComponent implements OnInit, OnDestroy, AfterViewInit { +export class CreateClusterComponent implements OnInit, OnDestroy { @ViewChild('skipConfirmTpl', { static: true }) skipConfirmTpl: TemplateRef; - currentStep: WizardStepModel; - currentStepSub: Subscription; + @ViewChild(TearsheetComponent) tearsheet!: TearsheetComponent; + permissions: Permissions; projectConstants: typeof AppConstants = AppConstants; - stepTitles: Step[] = [ - { - label: 'Add Hosts' - }, - { - label: 'Create OSDs', - complete: false - }, - { - label: 'Create Services', - complete: false - }, - { - label: 'Review', - complete: false - } + steps: Step[] = [ + { label: STEP_LABELS.ADD_HOSTS, invalid: false }, + { label: STEP_LABELS.CREATE_OSDS, invalid: false }, + { label: STEP_LABELS.CREATE_SERVICES, invalid: false }, + { label: STEP_LABELS.REVIEW, invalid: false } ]; - startClusterCreation = false; + title = $localize`Add Storage`; + description = $localize`Configure hosts, OSDs, and data services for your cluster.`; + submitButtonLabel = $localize`Add Storage`; + isSubmitLoading = false; + + startClusterCreation = true; observables: any = []; modalRef: NgbModalRef; driveGroup = new DriveGroup(); @@ -78,42 +75,22 @@ export class CreateClusterComponent implements OnInit, OnDestroy, AfterViewInit stepsToSkip: { [steps: string]: boolean } = {}; icons = Icons; - @Output() - submitAction = new EventEmitter(); - constructor( private authStorageService: AuthStorageService, - private wizardStepsService: WizardStepsService, private router: Router, private hostService: HostService, private notificationService: NotificationService, - private actionLabels: ActionLabelsI18n, private clusterService: ClusterService, private modalService: ModalCdsService, private taskWrapper: TaskWrapperService, private osdService: OsdService, - private route: ActivatedRoute, - private location: Location, - private changeDetectorRef: ChangeDetectorRef + private route: ActivatedRoute ) { this.permissions = this.authStorageService.getPermissions(); - this.currentStepSub = this.wizardStepsService - .getCurrentStep() - .subscribe((step: WizardStepModel) => { - this.currentStep = step; - }); - this.currentStep.stepIndex = 0; - } - ngAfterViewInit(): void { - this.changeDetectorRef.detectChanges(); } ngOnInit(): void { - this.stepTitles.forEach((steps, index) => { - steps.onClick = () => (this.currentStep.stepIndex = index); - }); this.route.queryParams.subscribe((params) => { - // reading 'welcome' value true/false to toggle add-storage wizand view and welcome view const showWelcomeScreen = params['welcome']; if (showWelcomeScreen) { this.startClusterCreation = showWelcomeScreen; @@ -125,15 +102,11 @@ export class CreateClusterComponent implements OnInit, OnDestroy, AfterViewInit this.selectedOption = { option: options.recommended_option, encrypted: false }; }); - this.stepTitles.forEach((stepTitle) => { - this.stepsToSkip[stepTitle.label] = false; + this.steps.forEach((step) => { + this.stepsToSkip[step.label] = false; }); } - onStepClick(step: WizardStepModel) { - this.wizardStepsService.setCurrentStep(step); - } - createCluster() { this.startClusterCreation = false; } @@ -162,8 +135,34 @@ export class CreateClusterComponent implements OnInit, OnDestroy, AfterViewInit this.modalService.show(ConfirmationModalComponent, modalVariables); } + onSkipOsdStep() { + this.stepsToSkip[STEP_LABELS.CREATE_OSDS] = true; + this.tearsheet.onNext(); + } + onSubmit() { - if (!this.stepsToSkip['Add Hosts']) { + const osdStepData = this.tearsheet?.getStepValueByLabel<{ + skipped: boolean; + driveGroup: DriveGroup; + selectedOption: object; + simpleDeployment: boolean; + }>(STEP_LABELS.CREATE_OSDS); + + if (osdStepData?.skipped) { + this.stepsToSkip[STEP_LABELS.CREATE_OSDS] = true; + } else if (osdStepData) { + if (osdStepData.driveGroup) { + this.driveGroup = osdStepData.driveGroup; + } + if (osdStepData.selectedOption) { + this.selectedOption = osdStepData.selectedOption; + } + if (osdStepData.simpleDeployment !== undefined) { + this.simpleDeployment = osdStepData.simpleDeployment; + } + } + + if (!this.stepsToSkip[STEP_LABELS.ADD_HOSTS]) { const hostContext = new CdTableFetchDataContext(() => undefined); this.hostService.list(hostContext.toParams(), 'false').subscribe((hosts) => { hosts.forEach((host) => { @@ -191,7 +190,7 @@ export class CreateClusterComponent implements OnInit, OnDestroy, AfterViewInit }); } - if (!this.stepsToSkip['Create OSDs']) { + if (!this.stepsToSkip[STEP_LABELS.CREATE_OSDS]) { if (this.driveGroup) { const user = this.authStorageService.getUsername(); this.driveGroup.setName(`dashboard-${user}-${_.now()}`); @@ -209,10 +208,7 @@ export class CreateClusterComponent implements OnInit, OnDestroy, AfterViewInit call: this.osdService.create([this.selectedOption], trackingId, 'predefined') }) .subscribe({ - error: (error) => error.preventDefault(), - complete: () => { - this.submitAction.emit(); - } + error: (error) => error.preventDefault() }); } else { if (this.osdService.osdDevices['totalDevices'] > 0) { @@ -228,7 +224,6 @@ export class CreateClusterComponent implements OnInit, OnDestroy, AfterViewInit .subscribe({ error: (error) => error.preventDefault(), complete: () => { - this.submitAction.emit(); this.osdService.osdDevices = []; } }); @@ -237,55 +232,7 @@ export class CreateClusterComponent implements OnInit, OnDestroy, AfterViewInit } } - setDriveGroup(driveGroup: DriveGroup) { - this.driveGroup = driveGroup; - } - - setDeploymentOptions(option: object) { - this.selectedOption = option; - } - - setDeploymentMode(mode: boolean) { - this.simpleDeployment = mode; - } - - onNextStep() { - if (!this.wizardStepsService.isLastStep()) { - this.wizardStepsService.getCurrentStep().subscribe((step: WizardStepModel) => { - this.currentStep = step; - }); - this.wizardStepsService.moveToNextStep(); - } else { - this.onSubmit(); - } - } - - onPreviousStep() { - if (!this.wizardStepsService.isFirstStep()) { - this.wizardStepsService.moveToPreviousStep(); - } else { - this.location.back(); - } - } - - onSkip() { - const stepTitle = this.stepTitles[this.currentStep.stepIndex]; - this.stepsToSkip[stepTitle.label] = true; - this.onNextStep(); - } - - showSubmitButtonLabel() { - return !this.wizardStepsService.isLastStep() ? this.actionLabels.NEXT : $localize`Add Storage`; - } - - showCancelButtonLabel() { - return !this.wizardStepsService.isFirstStep() - ? this.actionLabels.BACK - : this.actionLabels.CANCEL; - } - ngOnDestroy(): void { - this.currentStepSub.unsubscribe(); this.osdService.selectedFormValues = null; } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.html index e07a66893c4..a5b181968b2 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.html @@ -38,7 +38,8 @@ [columnNumbers]="{'lg': 13, 'md': 13, 'sm': 13}" class="tearsheet-main"> -
+
@@ -60,8 +61,22 @@ @if (currentStep === lastStep) { + i18n> + @if (isSubmitLoading) { + + + {{submitButtonLoadingLabel}}... + } + @else { + {{submitButtonLabel}} + } + } @else {