]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Converting add storage wizard into tearsheet 68977/head
authorDevika Babrekar <devika.babrekar@ibm.com>
Mon, 18 May 2026 14:23:22 +0000 (19:53 +0530)
committerDevika Babrekar <devika.babrekar@ibm.com>
Tue, 26 May 2026 13:24:57 +0000 (18:54 +0530)
Fixes: https://tracker.ceph.com/issues/76652
Signed-off-by: Devika Babrekar <devika.babrekar@ibm.com>
29 files changed:
src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/create-cluster.po.ts
src/pybind/mgr/dashboard/frontend/cypress/e2e/common/create-cluster/create-cluster.feature.po.ts
src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/03-create-cluster-create-services.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/04-create-cluster-create-osds.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/05-create-cluster-review.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/06-cluster-check.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.scss
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-review.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-1/create-cluster-step-1.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-1/create-cluster-step-1.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-1/create-cluster-step-1.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-2/create-cluster-step-2.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-2/create-cluster-step-2.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-2/create-cluster-step-2.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-3/create-cluster-step-3.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-3/create-cluster-step-3.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-3/create-cluster-step-3.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-4/create-cluster-step-4.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-4/create-cluster-step-4.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-4/create-cluster-step-4.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.scss
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.scss
src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.ts

index 00bd27633825ade9ffcae9855dd70513767077ed..c0c78b4ceb3b14464770efd6cdc1e2fd20fc0b57 100644 (file)
@@ -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() {
index b42ea14f7faa1e9338b1011e100ee98ce3a66cac..947b0431f6e7c8e919ffecfed06acee2130ead91 100644 (file)
@@ -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}', () => {
index a380a0e5d977030b05becb7e12b0cd0f86d7c46c..9c96d2a19ed93e1d53862053a2b8d2c444f4dbf5 100644 (file)
@@ -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', () => {
index d2c1e5b676965f6f9bb59f1ec9a084b30dd621ed..541fff0bcb4eb4cdd128107cdd1d43fb9c2c10ba 100644 (file)
@@ -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');
       }
     });
   });
index ad9930b01e2724d77ac19564996f4bd1690b4980..b81c388faab6eaff9020eb54462fe3a44e1d1b62 100644 (file)
@@ -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', () => {
index 35905ebd54958e3ba8cd9c1f818a2812140b4b21..103a145e1e6da76f2f71123f64352c4285177890 100644 (file)
@@ -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');
   });
 
index d636b5dc99657b3e1e7c5fda033c73ef3bc163bc..04c158cbc803546b0f6b1fb17230907223532a32 100644 (file)
@@ -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,
index beecca09671623526c4f815482e7e0f5de251827..1780a0cdc6463818bd948c5b174b3605877e3da0 100644 (file)
@@ -1,5 +1,7 @@
-cd-hosts {
-  ::ng-deep .nav {
-    display: none;
-  }
+.nav {
+  display: none;
+}
+
+.cds--row {
+  margin-inline: 0 !important;
 }
index e8c47e56ed6925935c186d0933e9f9c9ecc75b0e..ac6ec88eada0ed2d3ec694499ded6499e44e4056 100644 (file)
@@ -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 (file)
index 0000000..23ff0df
--- /dev/null
@@ -0,0 +1,15 @@
+<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
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 (file)
index 0000000..f5788bc
--- /dev/null
@@ -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 (file)
index 0000000..32d3599
--- /dev/null
@@ -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 (file)
index 0000000..c847a49
--- /dev/null
@@ -0,0 +1,25 @@
+<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>
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 (file)
index 0000000..12250e4
--- /dev/null
@@ -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 (file)
index 0000000..a144b3a
--- /dev/null
@@ -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<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 });
+  }
+}
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 (file)
index 0000000..4beaf46
--- /dev/null
@@ -0,0 +1,15 @@
+<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>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..a747869
--- /dev/null
@@ -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 (file)
index 0000000..984b16b
--- /dev/null
@@ -0,0 +1,9 @@
+<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
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..845c975
--- /dev/null
@@ -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({});
+  }
+}
index 5c7687bb3a297e3ef5fdfc0b483e86a2d95359f0..08dcae08f4fc19f410765b9cb535ff1198f6127a 100644 (file)
@@ -1,5 +1,5 @@
 @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>
index 3fb39ceef7885e57aff7eca77b4df4a2cd218e37..0503283fe5ea84f588333d65e4fededce89ab302 100644 (file)
@@ -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 {
index 01e9c6aa2a994f3c775c98f537472d5687da09b3..7ee0b057494247c5492f4d0867031d6ed714c1e7 100644 (file)
@@ -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<CreateClusterComponent>;
-  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: '<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();
   });
@@ -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();
   });
 });
index 133c489901fffbfb7a8b34ebb53566903f669902..cff79b5d365cf9e8ead42d2bf39856ce876c1020 100644 (file)
@@ -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<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();
@@ -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;
   }
 }
index e07a66893c43c264f1a6302cf1016e5117de14cd..a5b181968b2308da019aca60927f607f82c1a78f 100644 (file)
@@ -38,7 +38,8 @@
            [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
index a6ff590043fa561dc2f2a36a7b2565d18a0fd07e..f888e0bdf774fdf4024ab383f9a315007e39d7b7 100644 (file)
   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 {
index 4a8e9793025955c55f1e8ebaf9ee2a3044506d7b..c886b41bc1cd453393cb33392778df2e32200d23 100644 (file)
@@ -3,7 +3,7 @@ import { Component, ViewChild } from '@angular/core';
 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
@@ -13,6 +13,7 @@ import { ActivatedRoute } from '@angular/router';
       [steps]="steps"
       [title]="title"
       [description]="description"
+      [overflowScroll]="overflowScroll"
       (submitRequested)="onSubmit()"
     >
       <cd-tearsheet-step>
@@ -46,6 +47,7 @@ class MockHostComponent {
   ];
   title = 'Test Title';
   description = 'Test Description';
+  overflowScroll?: TearsheetOverflowScroll;
 
   onSubmit() {}
 
@@ -111,6 +113,27 @@ describe('TearsheetComponent', () => {
     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');
 
index 39c7cc304a0c04fb61916877d92e2a83bbca49c4..567f9a3f013f76192bb9bc73890206c4d382a465 100644 (file)
@@ -23,6 +23,8 @@ import { ConfirmationModalComponent } from '../confirmation-modal/confirmation-m
 import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
 import { Subject } from 'rxjs';
 
+export type TearsheetOverflowScroll = 'auto' | 'hidden' | 'visible' | 'scroll';
+
 /**
 <cd-tearsheet
     [steps]="steps"
@@ -66,7 +68,9 @@ export class TearsheetComponent implements OnInit, AfterViewInit, OnDestroy {
   @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>();
@@ -87,6 +91,13 @@ export class TearsheetComponent implements OnInit, AfterViewInit, OnDestroy {
     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;
@@ -171,12 +182,12 @@ export class TearsheetComponent implements OnInit, AfterViewInit, OnDestroy {
 
   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;