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() {
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}', () => {
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', () => {
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', () => {
// 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');
}
});
});
onboarding.navigateTo();
onboarding.onboarding();
- cy.get('cd-wizard').within(() => {
- cy.get('button').contains('Review').click();
- });
+ onboarding.selectStep('Review');
});
describe('fields check', () => {
// 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');
});
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';
PlacementPipe,
CreateClusterComponent,
CreateClusterReviewComponent,
+ CreateClusterStep1Component,
+ CreateClusterStep2Component,
+ CreateClusterStep3Component,
+ CreateClusterStep4Component,
UpgradeComponent,
UpgradeStartModalComponent,
UpgradeProgressComponent,
-cd-hosts {
- ::ng-deep .nav {
- display: none;
- }
+.nav {
+ display: none;
+}
+
+.cds--row {
+ margin-inline: 0 !important;
}
-import { Component, OnInit } from '@angular/core';
+import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import _ from 'lodash';
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[] = [];
--- /dev/null
+<div cdsGrid
+ [useCssGrid]="true"
+ [narrow]="true"
+ [fullWidth]="true">
+ <div cdsCol
+ [columnNumbers]="{sm: 4, md: 8, lg: 16}">
+ <h4 i18n>Add Hosts</h4>
+
+ <cd-hosts [hiddenColumns]="['service_instances']"
+ [hideMaintenance]="true"
+ [hasTableDetails]="false"
+ [showGeneralActionsOnly]="true"
+ [showExpandClusterBtn]="false"></cd-hosts>
+ </div>
+</div>
\ No newline at end of file
--- /dev/null
+.nav {
+ display: none;
+}
--- /dev/null
+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({});
+ }
+}
--- /dev/null
+<div cdsGrid
+ [useCssGrid]="true"
+ [narrow]="true"
+ [fullWidth]="true">
+ <div cdsCol
+ [columnNumbers]="{sm: 4, md: 8, lg: 16}">
+
+ <h4 i18n>Create OSDs</h4>
+
+ <div>
+ <cd-osd-form [hideTitle]="true"
+ [hideSubmitBtn]="true"
+ (emitDriveGroup)="setDriveGroup($event)"
+ (emitDeploymentOption)="setDeploymentOptions($event)"
+ (emitMode)="setDeploymentMode($event)"></cd-osd-form>
+ </div>
+
+ <button cdsButton="secondary"
+ class="cds-mt-4"
+ id="skipStepBtn"
+ (click)="onSkip()"
+ aria-label="Skip this step"
+ i18n>Skip</button>
+ </div>
+</div>
--- /dev/null
+cd-osd-form {
+ .card {
+ border: 0;
+ }
+
+ .accordion {
+ margin-left: -1.5rem;
+ }
+}
--- /dev/null
+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<void>();
+
+ 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 });
+ }
+}
--- /dev/null
+<div cdsGrid
+ [useCssGrid]="true"
+ [narrow]="true"
+ [fullWidth]="true">
+ <div cdsCol
+ [columnNumbers]="{sm: 4, md: 8, lg: 16}">
+
+ <h4 18n>Create Services</h4>
+
+ <cd-services [hasDetails]="false"
+ [hiddenServices]="['mon', 'mgr', 'crash', 'agent']"
+ [hiddenColumns]="['status.running', 'status.size', 'status.last_refresh']"
+ [routedModal]="false"></cd-services>
+ </div>
+</div>
--- /dev/null
+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({});
+ }
+}
--- /dev/null
+<div cdsGrid
+ [useCssGrid]="true"
+ [narrow]="true"
+ [fullWidth]="true">
+ <div cdsCol
+ [columnNumbers]="{sm: 4, md: 8, lg: 16}">
+ <cd-create-cluster-review></cd-create-cluster-review>
+ </div>
+</div>
\ No newline at end of file
--- /dev/null
+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({});
+ }
+}
@if (startClusterCreation) {
-<div class="container-fluid">
+<div class="container-fluid-main">
<div cdsRow
class="cds-ml-5 cds-mt-6">
<div cdsCol
}
@else {
-<div cdsRow
- class="form cds-mt-6">
- <div cdsCol
- [columnNumbers]="{'lg': 2, 'md': 2, 'sm': 2}"
- class="indicator-wrapper">
-
- <div class="form-header"
- i18n>Add Storage</div>
- <cd-wizard [stepsTitle]="stepTitles"></cd-wizard>
- </div>
-
- <div cdsCol
- [columnNumbers]="{'lg': 14, 'md': 14, 'sm': 14}">
- <ng-container [ngSwitch]="currentStep?.stepIndex">
- <div *ngSwitchCase="0"
- class="ms-5">
- <h4 class="title"
- i18n>Add Hosts</h4>
-
- <cd-hosts [hiddenColumns]="['service_instances']"
- [hideMaintenance]="true"
- [hasTableDetails]="false"
- [showGeneralActionsOnly]="true"
- [showExpandClusterBtn]="false"></cd-hosts>
- </div>
- <div *ngSwitchCase="1"
- class="ms-5">
- <h4 class="title"
- i18n>Create OSDs</h4>
- <div class="alignForm">
- <cd-osd-form [hideTitle]="true"
- [hideSubmitBtn]="true"
- (emitDriveGroup)="setDriveGroup($event)"
- (emitDeploymentOption)="setDeploymentOptions($event)"
- (emitMode)="setDeploymentMode($event)"></cd-osd-form>
- </div>
- </div>
- <div *ngSwitchCase="2"
- class="ms-5">
- <h4 class="title"
- i18n>Create Services</h4>
- <cd-services [hasDetails]="false"
- [hiddenServices]="['mon', 'mgr', 'crash', 'agent']"
- [hiddenColumns]="['status.running', 'status.size', 'status.last_refresh']"
- [routedModal]="false"></cd-services>
- </div>
- <div *ngSwitchCase="3"
- class="ms-5">
- <cd-create-cluster-review></cd-create-cluster-review>
- </div>
- </ng-container>
- <div cdsRow
- class="m-5">
- @if (stepTitles[currentStep?.stepIndex]?.label === 'Create OSDs') {
- <button cdsButton="secondary"
- class="me-3"
- id="skipStepBtn"
- (click)="onSkip()"
- aria-label="Skip this step"
- i18n>Skip</button>
- }
- <cd-back-button buttonType="secondary"
- aria-label="Close"
- (backAction)="onPreviousStep()"
- [name]="showCancelButtonLabel()"></cd-back-button>
- <button cdsButton="primary"
- (click)="onNextStep()"
- aria-label="Next"
- i18n>{{ showSubmitButtonLabel() }}</button>
- </div>
- </div>
-</div>
+<cd-tearsheet
+ type="full"
+ [steps]="steps"
+ [title]="title"
+ [description]="description"
+ [submitButtonLabel]="submitButtonLabel"
+ [isSubmitLoading]="isSubmitLoading"
+ (submitRequested)="onSubmit()"
+ [overflowScroll]="'auto'">
+ <cd-tearsheet-step>
+ <cd-create-cluster-step-1 #tearsheetStep></cd-create-cluster-step-1>
+ </cd-tearsheet-step>
+ <cd-tearsheet-step>
+ <cd-create-cluster-step-2
+ #tearsheetStep
+ (skipStep)="onSkipOsdStep()"></cd-create-cluster-step-2>
+ </cd-tearsheet-step>
+ <cd-tearsheet-step>
+ <cd-create-cluster-step-3 #tearsheetStep></cd-create-cluster-step-3>
+ </cd-tearsheet-step>
+ <cd-tearsheet-step>
+ <cd-create-cluster-step-4 #tearsheetStep></cd-create-cluster-step-4>
+ </cd-tearsheet-step>
+</cd-tearsheet>
}
<ng-template #skipConfirmTpl>
@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 {
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<CreateClusterComponent>;
- let wizardStepService: WizardStepsService;
let hostService: HostService;
let osdService: OsdService;
let modalServiceShowSpy: jasmine.Spy;
});
beforeEach(() => {
+ TestBed.overrideComponent(CreateClusterStep3Component, {
+ set: { template: '<div class="create-cluster-step-3"></div>' }
+ });
+
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();
});
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();
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();
});
});
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<any>;
- 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();
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;
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;
}
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) => {
});
}
- 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()}`);
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) {
.subscribe({
error: (error) => error.preventDefault(),
complete: () => {
- this.submitAction.emit();
this.osdService.osdDevices = [];
}
});
}
}
- 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;
}
}
[columnNumbers]="{'lg': 13, 'md': 13, 'sm': 13}"
class="tearsheet-main">
<!-- Tearsheet Content Area -->
- <div class="tearsheet-content tearsheet-content--full">
+ <div class="tearsheet-content tearsheet-content--full"
+ [ngStyle]="contentOverflowStyle">
<ng-container
*ngTemplateOutlet="activeStepTemplate">
</ng-container>
@if (currentStep === lastStep) {
<button cdsButton="primary"
size="xl"
+ class="tearsheet-footer-submit"
+ [disabled]="isSubmitLoading"
(click)="onSubmit()"
- i18n>{{submitButtonLabel}}</button>
+ i18n>
+ @if (isSubmitLoading) {
+ <cds-loading
+ [isActive]="isSubmitLoading"
+ [overlay]="false"
+ size="sm">
+ </cds-loading>
+ {{submitButtonLoadingLabel}}...
+ }
+ @else {
+ {{submitButtonLabel}}
+ }
+ </button>
}
@else {
<button cdsButton="primary"
<div
cdsCol
class="tearsheet-content"
- [columnNumbers]="{ lg: 10 }">
+ [columnNumbers]="{ lg: 10 }"
+ [ngStyle]="contentOverflowStyle">
<ng-container *ngTemplateOutlet="activeStepTemplate"></ng-container>
</div>
<aside
}
@else {
<!-- Tearsheet content without right influencer -->
- <div class="tearsheet-content">
+ <div class="tearsheet-content"
+ [ngStyle]="contentOverflowStyle">
<ng-container *ngTemplateOutlet="activeStepTemplate"></ng-container>
</div>
}
size="xl"
class="tearsheet-footer-submit"
[disabled]="isSubmitLoading"
- (click)="handleSubmit()"
+ (click)="onSubmit()"
i18n>
@if (isSubmitLoading) {
<cds-loading
width: 100%;
margin: 0;
padding: 0;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+
+ .tearsheet-cols--full {
+ flex: 1 1 auto;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .tearsheet-body {
+ flex: 1 1 auto;
+ min-height: 0;
+ }
+
+ .tearsheet-main {
+ min-height: 0;
+ }
+
+ .tearsheet-content--full {
+ flex: 1 1 auto;
+ min-height: 0;
+ min-block-size: 0;
+ max-block-size: none;
+ }
}
.tearsheet-cols--full {
import { By } from '@angular/platform-browser';
import { SharedModule } from '../../shared.module';
import { TearsheetStepComponent } from '../tearsheet-step/tearsheet-step.component';
-import { TearsheetComponent } from './tearsheet.component';
+import { TearsheetComponent, TearsheetOverflowScroll } from './tearsheet.component';
import { ActivatedRoute } from '@angular/router';
// Mock Component that uses tearsheet
[steps]="steps"
[title]="title"
[description]="description"
+ [overflowScroll]="overflowScroll"
(submitRequested)="onSubmit()"
>
<cd-tearsheet-step>
];
title = 'Test Title';
description = 'Test Description';
+ overflowScroll?: TearsheetOverflowScroll;
onSubmit() {}
expect(step1Content.nativeElement.textContent).toContain('Step 1 Content');
});
+ describe('overflowScroll', () => {
+ it('should not set inline overflow when overflowScroll is unset', () => {
+ const content = hostFixture.debugElement.query(By.css('.tearsheet-content'));
+ expect(content.nativeElement.style.overflow).toBe('');
+ });
+
+ it('should apply overflow style when overflowScroll is set', () => {
+ hostComponent.overflowScroll = 'hidden';
+ hostFixture.detectChanges();
+ const content = hostFixture.debugElement.query(By.css('.tearsheet-content'));
+ expect(content.nativeElement.style.overflow).toBe('hidden');
+ });
+
+ it('should enable scrolling when overflowScroll is auto', () => {
+ hostComponent.overflowScroll = 'auto';
+ hostFixture.detectChanges();
+ const content = hostFixture.debugElement.query(By.css('.tearsheet-content'));
+ expect(content.nativeElement.style.overflow).toBe('auto');
+ });
+ });
+
it('should emit submitRequested event', () => {
spyOn(hostComponent, 'onSubmit');
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Subject } from 'rxjs';
+export type TearsheetOverflowScroll = 'auto' | 'hidden' | 'visible' | 'scroll';
+
/**
<cd-tearsheet
[steps]="steps"
@Input() size: 'xs' | 'sm' | 'md' | 'lg' = 'lg';
@Input() submitButtonLabel: string = $localize`Create`;
@Input() submitButtonLoadingLabel: string = $localize`Creating`;
- @Input() isSubmitLoading: boolean = true;
+ @Input() isSubmitLoading: boolean = false;
+ /** When set, applies `overflow` on the tearsheet content area; omit to use stylesheet defaults. */
+ @Input() overflowScroll?: TearsheetOverflowScroll;
@Output() submitRequested = new EventEmitter<void>();
@Output() closeRequested = new EventEmitter<void>();
return this.stepContents?.toArray()[this.currentStep]?.showRightInfluencer;
}
+ get contentOverflowStyle(): { overflow: TearsheetOverflowScroll } | null {
+ if (!this.overflowScroll) {
+ return null;
+ }
+ return { overflow: this.overflowScroll };
+ }
+
getStepValue<T = any>(index: number): T | null {
const wrapper = this.stepContents?.toArray()?.[index];
return wrapper?.stepComponent?.formGroup?.value ?? null;
getMergedPayload(): any {
return this.stepContents.toArray().reduce((acc, wrapper) => {
- const stepFormValue = wrapper.stepComponent.formGroup.value;
+ const stepFormValue = wrapper.stepComponent?.formGroup?.value;
return { ...acc, ...stepFormValue };
}, {});
}
- handleSubmit() {
+ onSubmit() {
this.stepContents?.forEach((wrapper, index) => {
const form = wrapper.stepComponent?.formGroup;
if (!form) return;