]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: carbonized OSD form component 68031/head
authorSyed Ali Ul Hasan <syedaliulhasan19@gmail.com>
Wed, 10 Jun 2026 17:30:36 +0000 (23:00 +0530)
committerSyed Ali Ul Hasan <syedaliulhasan19@gmail.com>
Wed, 10 Jun 2026 17:30:36 +0000 (23:00 +0530)
Fixes: https://tracker.ceph.com/issues/68265
Signed-off-by: Syed Ali Ul Hasan <syedaliulhasan19@gmail.com>
15 files changed:
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-step-2/create-cluster-step-2.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster-step-2/create-cluster-step-2.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.scss
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-devices-selection-groups/osd-devices-selection-groups.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.scss
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-form/osd-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/osd-form.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_spacings.scss

index a711704ad6ec556bafae24f509c67dfde38186ad..3edd74a43978d3e7dcc7606f9c16aa71a3a067ce 100644 (file)
@@ -27,7 +27,8 @@ import {
   TabsModule,
   RadioModule,
   TilesModule,
-  LayerModule
+  LayerModule,
+  AccordionModule
 } from 'carbon-components-angular';
 import Analytics from '@carbon/icons/es/analytics/16';
 import CloseFilled from '@carbon/icons/es/close--filled/16';
@@ -145,7 +146,8 @@ import { TextLabelListComponent } from '~/app/shared/components/text-label-list/
     FileUploaderModule,
     RadioModule,
     TilesModule,
-    LayerModule
+    LayerModule,
+    AccordionModule
   ],
   declarations: [
     MonitorComponent,
index c847a491e7d2fc48518a5f43b0255e68b6fac404..1523bdb986bd930be286df8300b4a58678c747c0 100644 (file)
@@ -8,11 +8,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>
+      @if (showForm) {
+        <cd-osd-form [hideTitle]="true"
+                     (emitDriveGroup)="setDriveGroup($event)"
+                     (emitDeploymentOption)="setDeploymentOptions($event)"
+                     (emitMode)="setDeploymentMode($event)"
+                     (osdCreated)="onOsdCreated()"></cd-osd-form>
+      } @else {
+        <cd-osd-list [showTabs]="false"
+                     (createAction)="onCreateAction()"></cd-osd-list>
+      }
     </div>
 
     <button cdsButton="secondary"
index a144b3ac7fd029f0349014147f1864aa02e46e5a..655e675f184863e4a911cab003374acbc1a8e30d 100644 (file)
@@ -16,6 +16,7 @@ export class CreateClusterStep2Component implements OnInit, TearsheetStep {
   @Output() skipStep = new EventEmitter<void>();
 
   formGroup: CdFormGroup;
+  showForm = false;
 
   ngOnInit() {
     this.formGroup = new CdFormGroup({
@@ -31,6 +32,15 @@ export class CreateClusterStep2Component implements OnInit, TearsheetStep {
     this.skipStep.emit();
   }
 
+  onOsdCreated() {
+    this.showForm = false;
+    this.formGroup.patchValue({ skipped: true });
+  }
+
+  onCreateAction() {
+    this.showForm = true;
+  }
+
   setDriveGroup(driveGroup: DriveGroup) {
     this.formGroup.patchValue({ driveGroup });
   }
index de55256de2bdf54970ad5ab9fa8c0f179b6f8cb3..5e0b0c90cc2c0c89aed63ff97ab682a2af41e0c2 100644 (file)
-<!-- button -->
-<div class="form-group row">
-  <label class="cd-col-form-label"
+<div class="form-group row osd-devices-selection-row">
+  <label class="cd-form-label"
          for="createDeleteButton">
     <ng-container i18n>{{ name }} devices</ng-container>
     <cd-helper>
-      <span i18n
-            *ngIf="type === 'data'">The primary storage devices. These devices contain all OSD data.</span>
-      <span i18n
-            *ngIf="type === 'wal'">Write-Ahead-Log devices. These devices are used for BlueStore’s internal journal. It is only useful to use a WAL device if the device is faster than the primary device (e.g. NVME devices or SSDs). If there is only a small amount of fast storage available (e.g., less than a gigabyte), we recommend using it as a WAL device.</span>
-      <span i18n
-            *ngIf="type === 'db'">DB devices can be used for storing BlueStore’s internal metadata.  It is only helpful to provision a DB device if it is faster than the primary device (e.g. NVME devices or SSD).</span>
+      @if (type === 'data') {
+      <span i18n>The primary storage devices. These devices contain all OSD data.</span>
+      }
+      @if (type === 'wal') {
+      <span i18n>Write-Ahead-Log devices. These devices are used for BlueStore’s internal journal. It is only useful to use a WAL device if the device is faster than the primary device (e.g. NVME devices or SSDs). If there is only a small amount of fast storage available (e.g., less than a gigabyte), we recommend using it as a WAL device.</span>
+      }
+      @if (type === 'db') {
+      <span i18n>DB devices can be used for storing BlueStore’s internal metadata.  It is only helpful to provision a DB device if it is faster than the primary device (e.g. NVME devices or SSD).</span>
+      }
     </cd-helper>
   </label>
   <div class="cd-col-form-input">
-    <ng-container *ngIf="devices.length === 0; else blockClearDevices">
-      <button type="button"
-              class="btn btn-light"
-              (click)="showSelectionModal()"
-              data-toggle="tooltip"
-              [title]="addButtonTooltip"
-              [disabled]="availDevices.length === 0 || !canSelect || expansionCanSelect">
-        <svg [cdsIcon]="icons.add"
-             [size]="icons.size16"
-             ></svg>
-        <ng-container i18n>Add</ng-container>
-      </button>
-    </ng-container>
-    <ng-template #blockClearDevices>
-      <div class="pb-2 my-2 border-bottom">
-        <span *ngFor="let filter of appliedFilters">
-          <cds-tag class="tag-dark me-2">{{ filter.name }}: {{ filter.value.formatted }}</cds-tag>
-        </span>
-        <a class="tc_clearSelections"
-           href=""
-           (click)="clearDevices(); false">
-          <svg [cdsIcon]="icons.clearFilters"
-               [size]="icons.size16"
-               ></svg>
-          <ng-container i18n>Clear</ng-container>
-        </a>
-      </div>
-      <div>
-        <cd-inventory-devices [devices]="devices"
-                              [hiddenColumns]="['available', 'osd_ids']"
-                              [filterColumns]="[]">
-        </cd-inventory-devices>
-      </div>
-      <div *ngIf="type === 'data'"
-           class="float-end">
-        <span i18n>Raw capacity: {{ capacity | dimlessBinary }}</span>
-      </div>
-    </ng-template>
+    @if (devices.length === 0 && inlineSelection) {
+    @if (!canSelect) {
+    <cd-alert-panel type="info"
+                    size="slim"
+                    [showTitle]="false">
+      <ng-container i18n>{{ tooltips.addPrimaryFirst }}</ng-container>
+    </cd-alert-panel>
+    }
+    @if (canSelect) {
+    @if (availDevices.length === 0) {
+    <cd-alert-panel type="warning"
+                    size="slim"
+                    [showTitle]="false">
+      <ng-container i18n>No available devices</ng-container>
+    </cd-alert-panel>
+    }
+    @if (availDevices.length > 0 && !canInlineSubmit) {
+    <cd-alert-panel type="warning"
+                    size="slim"
+                    [showTitle]="false">
+      <ng-container i18n>At least one of these filters must be applied in order to proceed:</ng-container>
+      @for (filter of requiredFilters; track filter) {
+      <cds-tag class="tag-dark ms-2">{{ filter }}</cds-tag>
+      }
+    </cd-alert-panel>
+    }
+    <cd-inventory-devices [devices]="availDevices"
+                          [hostname]="hostname"
+                          [diskType]="name === 'Primary' ? 'hdd' : 'ssd'"
+                          [hiddenColumns]="['available', 'osd_ids']"
+                          [filterColumns]="filterColumns"
+                          (filterChange)="onInlineFilterChange($event)">
+    </cd-inventory-devices>
+    @if (canInlineSubmit) {
+    <div class="text-center cds-mt-3 cds-mb-3">
+      <span i18n>Number of devices: {{ inlineFilteredDevices.length }}. Raw capacity:
+        {{ inlineCapacity | dimlessBinary }}.</span>
+    </div>
+    }
+    <button type="button"
+            class="btn btn-light"
+            (click)="submitInlineSelection()"
+            [disabled]="!canInlineSubmit || inlineFilteredDevices.length === 0">
+      <svg [cdsIcon]="icons.add"
+           [size]="icons.size16"></svg>
+      <ng-container i18n>Add</ng-container>
+    </button>
+    }
+    } @else {
+    @if (devices.length === 0) {
+    <button type="button"
+            class="btn btn-light"
+            (click)="showSelectionModal()"
+            data-toggle="tooltip"
+            [title]="addButtonTooltip"
+            [disabled]="availDevices.length === 0 || !canSelect || expansionCanSelect">
+      <svg [cdsIcon]="icons.add"
+           [size]="icons.size16"></svg>
+      <ng-container i18n>Add</ng-container>
+    </button>
+    } @else {
+    <div class="pb-2 my-2 border-bottom">
+      <span *ngFor="let filter of appliedFilters">
+        <cds-tag class="tag-dark me-2">{{ filter.name }}: {{ filter.value.formatted }}</cds-tag>
+      </span>
+      <a class="tc_clearSelections"
+         href=""
+         (click)="clearDevices(); false">
+        <svg [cdsIcon]="icons.clearFilters"
+             [size]="icons.size16"></svg>
+        <ng-container i18n>Clear</ng-container>
+      </a>
+    </div>
+    <div>
+      <cd-inventory-devices [devices]="devices"
+                            [hiddenColumns]="['available', 'osd_ids']"
+                            [filterColumns]="[]">
+      </cd-inventory-devices>
+    </div>
+    @if (type === 'data') {
+    <div class="float-end">
+      <span i18n>Raw capacity: {{ capacity | dimlessBinary }}</span>
+    </div>
+    }
+    }
+    }
   </div>
 </div>
index 3fb8f6b3848265a33bf92c770feddbb9f45a5cbe..bf3223c234d17cd1c9252807c207f248b69fbe1f 100644 (file)
@@ -1,3 +1,17 @@
 .tc_clearSelections {
   text-decoration: none;
 }
+
+.osd-devices-selection-row {
+  .cd-form-label,
+  .cd-col-form-input {
+    flex: 0 0 100%;
+    max-width: 100%;
+    width: 100%;
+  }
+
+  cd-inventory-devices {
+    display: block;
+    width: 100%;
+  }
+}
index 5acb7c51b992a44aee159e2600ddb7e38fa62716..79d03d10b45d67f0bf3e8718c1f84b336549665f 100644 (file)
@@ -31,6 +31,8 @@ export class OsdDevicesSelectionGroupsComponent implements OnInit, OnChanges {
 
   @Input() canSelect: boolean;
 
+  @Input() inlineSelection = false;
+
   @Output()
   selected = new EventEmitter<DevicesSelectionChangeEvent>();
 
@@ -45,6 +47,23 @@ export class OsdDevicesSelectionGroupsComponent implements OnInit, OnChanges {
   isOsdPage: boolean;
 
   addButtonTooltip: String;
+  filterColumns = [
+    'hostname',
+    'human_readable_type',
+    'sys_api.vendor',
+    'sys_api.model',
+    'sys_api.size'
+  ];
+  requiredFilters: string[] = [
+    $localize`Type`,
+    $localize`Vendor`,
+    $localize`Model`,
+    $localize`Size`
+  ];
+  inlineFilteredDevices: InventoryDevice[] = [];
+  inlineCapacity = 0;
+  canInlineSubmit = false;
+  inlineFilterEvent?: CdTableColumnFiltersChange;
   tooltips = {
     noAvailDevices: $localize`No available devices`,
     addPrimaryFirst: $localize`Please add primary devices first`,
@@ -77,39 +96,65 @@ export class OsdDevicesSelectionGroupsComponent implements OnInit, OnChanges {
   }
 
   showSelectionModal() {
-    const filterColumns = [
-      'hostname',
-      'human_readable_type',
-      'sys_api.vendor',
-      'sys_api.model',
-      'sys_api.size'
-    ];
     const diskType = this.name === 'Primary' ? 'hdd' : 'ssd';
     const initialState = {
       hostname: this.hostname,
       deviceType: this.name,
       diskType: diskType,
       devices: this.availDevices,
-      filterColumns: filterColumns
+      filterColumns: this.filterColumns
     };
     const modalRef = this.modalService.show(OsdDevicesSelectionModalComponent, initialState, {
       size: 'xl'
     });
     modalRef.componentInstance.submitAction.subscribe((result: CdTableColumnFiltersChange) => {
-      this.devices = result.data;
-      this.capacity = _.sumBy(this.devices, 'sys_api.size');
-      this.appliedFilters = result.filters;
-      const event = _.assign({ type: this.type }, result);
-      if (!this.isOsdPage) {
-        this.osdService.osdDevices[this.type] = this.devices;
-        this.osdService.osdDevices['disableSelect'] =
-          this.canSelect || this.devices.length === this.availDevices.length;
-        this.osdService.osdDevices[this.type]['capacity'] = this.capacity;
-      }
-      this.selected.emit(event);
+      this.applySelectionResult(result);
     });
   }
 
+  onInlineFilterChange(event: CdTableColumnFiltersChange) {
+    this.inlineCapacity = 0;
+    this.canInlineSubmit = false;
+    this.inlineFilterEvent = undefined;
+
+    if (_.isEmpty(event.filters)) {
+      this.inlineFilteredDevices = [];
+      return;
+    }
+
+    const filters = event.filters.filter((filter) => filter.prop !== 'hostname');
+    this.canInlineSubmit = !_.isEmpty(filters);
+    this.inlineFilteredDevices = event.data;
+    this.inlineCapacity = _.sumBy(this.inlineFilteredDevices, 'sys_api.size');
+    this.inlineFilterEvent = event;
+  }
+
+  submitInlineSelection() {
+    if (
+      !this.inlineFilterEvent ||
+      !this.canInlineSubmit ||
+      this.inlineFilteredDevices.length === 0
+    ) {
+      return;
+    }
+
+    this.applySelectionResult(this.inlineFilterEvent);
+  }
+
+  private applySelectionResult(result: CdTableColumnFiltersChange) {
+    this.devices = result.data;
+    this.capacity = _.sumBy(this.devices, 'sys_api.size');
+    this.appliedFilters = result.filters;
+    const event = _.assign({ type: this.type }, result);
+    if (!this.isOsdPage) {
+      this.osdService.osdDevices[this.type] = this.devices;
+      this.osdService.osdDevices['disableSelect'] =
+        this.canSelect || this.devices.length === this.availDevices.length;
+      this.osdService.osdDevices[this.type]['capacity'] = this.capacity;
+    }
+    this.selected.emit(event);
+  }
+
   private updateAddButtonTooltip() {
     if (this.type === 'data' && this.availDevices.length === 0) {
       this.addButtonTooltip = this.tooltips.noAvailDevices;
@@ -136,6 +181,10 @@ export class OsdDevicesSelectionGroupsComponent implements OnInit, OnChanges {
       clearedDevices: [...this.devices]
     };
     this.devices = [];
+    this.inlineFilteredDevices = [];
+    this.inlineCapacity = 0;
+    this.canInlineSubmit = false;
+    this.inlineFilterEvent = undefined;
     this.cleared.emit(event);
   }
 }
index d232aba2339499f872d489cd41d7d0963f69a74e..d536d41a9c62c85d7b4d34f795bd486dbd6c9f19 100644 (file)
-<cd-orchestrator-doc-panel *ngIf="!hasOrchestrator"></cd-orchestrator-doc-panel>
-
-<div class="card"
-     *cdFormLoading="loading">
-  <div i18n="form title|Example: Create Pool@@formTitle"
-       class="card-header"
-       *ngIf="!hideTitle">{{ action | titlecase }} {{ resource | upperFirst }}</div>
-  <div class="card-body ms-2">
-    <form name="form"
-          #formDir="ngForm"
-          [formGroup]="form"
-          novalidate>
-      <cd-alert-panel *ngIf="availDevices?.length === 0"
-                      type="warning"
-                      class="mx-3"
-                      i18n>
-        <div cdsStack="vertical"
-             [gap]="2">
-          <span class="cds--type-heading-compact-01">No eligible devices found for OSD creation.</span>
-          <span class="cds--type-body-compact-01">Physical disks may be present, but none meet the requirements (unused, unformatted, and not already configured by Ceph).</span>
-        </div>
-      </cd-alert-panel>
-      <div class="accordion">
-        <div class="accordion-item">
-          <h2 class="accordion-header">
-            <button class="accordion-button"
-                    type="button"
-                    data-toggle="collapse"
-                    aria-label="toggle deployment options"
-                    [ngClass]="{collapsed: !simpleDeployment}"
-                    (click)="emitDeploymentMode()"
-                    i18n>Deployment Options</button>
-          </h2>
-        </div>
-        <div class="accordion-collapse collapse"
-             [ngClass]="{show: simpleDeployment}">
-          <div class="accordion-body">
-            <div class="pt-3 pb-3"
-                 *ngFor="let optionName of optionNames">
-              <div class="custom-control form-check custom-control-inline">
-                <input class="form-check-input"
-                       type="radio"
-                       name="deploymentOption"
-                       [id]="optionName"
-                       [value]="optionName"
-                       formControlName="deploymentOption"
-                       (change)="emitDeploymentSelection()"
-                       [attr.disabled]="!deploymentOptions?.options[optionName].available ? true : null">
-                <label class="form-check-label"
-                       [id]="'label_' + optionName"
-                       [for]="optionName"
-                       i18n>{{ deploymentOptions?.options[optionName].title }}
-                       {{ deploymentOptions?.recommended_option === optionName ? "(Recommended)" : "" }}
-                  <cd-helper>
-                    <span>{{ deploymentOptions?.options[optionName].desc }}</span>
-                  </cd-helper>
-                </label>
+@if (!hasOrchestrator) {
+  <cd-orchestrator-doc-panel></cd-orchestrator-doc-panel>
+}
+
+<cd-tearsheet
+  [steps]="steps"
+  [title]="!hideTitle ? (action | titlecase) + ' ' + (resource | upperFirst) : ''"
+  (submitRequested)="submit()"
+  (stepChanged)="populateReviewData()"
+  [isSubmitLoading]="isSubmitLoading"
+  [submitButtonLabel]="simpleDeployment ? createOsdsLabel : actionLabels.PREVIEW"
+  type="full">
+
+  <cd-tearsheet-step>
+  <div class="osd-tearsheet-content">
+    <form
+    [formGroup]="form"
+    cdsStack="vertical"
+    [gap]="6">
+
+    @if (availDevices?.length === 0) {
+    <cd-alert-panel
+      type="warning"
+      class="osd-alert-block"
+      i18n>
+      <div
+      cdsStack="vertical"
+      [gap]="2">
+      <span class="cds--type-heading-compact-01">
+        No eligible devices found for OSD creation.
+      </span>
+      <span class="cds--type-body-compact-01">
+        Physical disks may be present, but none meet the requirements
+        (unused, unformatted, and not already configured by Ceph).
+      </span>
+      </div>
+    </cd-alert-panel>
+    }
+
+    <div>
+      <cds-text-label
+      class="cds--type-heading-compact-02 cds-mb-5"
+      i18n>
+      Deployment Options
+      </cds-text-label>
+
+      <div cdsStack="vertical"
+           [gap]="4">
+        <cds-radio-group
+          formControlName="deploymentMode"
+          orientation="vertical">
+
+          <cds-radio value="automatic">
+            <div class="osd-radio-label-wrapper">
+              <span
+              class="cds--type-body-short-01"
+              i18n>
+                Automatic
+              </span>
+              <div
+              class="cds--type-helper-text-01 osd-radio-helper-text cds-mt-2"
+              i18n>
+                Choose a pre-configured profile for you.
               </div>
             </div>
-            <!-- @TODO: Visualize the storage used on a chart -->
-            <!-- <div class="pie-chart">
-              <h4 class="text-center">Selected Capacity</h4>
-              <h5 class="margin text-center">10 Hosts | 30 NVMes </h5>
-              <div class="char-i-contain">
-                <cd-health-pie [data]="data"
-                               [config]="rawCapacityChartConfig"
-                               [isBytesData]="true"
-                               (prepareFn)="prepareRawUsage($event[0], $event[1])">
-                </cd-health-pie>
+          </cds-radio>
+
+        @if (form.get('deploymentMode').value !== 'manual') {
+        <div class="cds-pl-7 cds-mb-5"
+             cdsStack="vertical"
+             [gap]="4">
+          <cds-text-label
+            class="cds--type-heading-compact-02"
+            i18n>
+            Pre-configured profiles
+          </cds-text-label>
+
+          <cds-radio-group
+            formControlName="deploymentOption"
+            (change)="emitDeploymentSelection()"
+            orientation="vertical">
+
+            @for (optionName of optionNames; track optionName; let isLast = $last) {
+            <cds-radio
+              [value]="optionName"
+              [class.cds-mb-5]="!isLast"
+              [disabled]="!deploymentOptions?.options[optionName].available ? true : null">
+
+              <div class="osd-radio-label-wrapper">
+              <span class="cds--type-body-short-01">
+                {{ deploymentOptions?.options[optionName].title }}
+                @if (deploymentOptions?.recommended_option === optionName) {
+                <strong i18n>(Recommended)</strong>
+                }
+              </span>
+
+              <div class="cds--type-helper-text-01 osd-radio-helper-text cds-mt-2">
+                {{ deploymentOptions?.options[optionName].desc }}
               </div>
-            </div> -->
-          </div>
-        </div>
-        <div class="accordion-item">
-          <h2 class="accordion-header">
-            <button class="accordion-button"
-                    type="button"
-                    aria-label="toggle advanced mode"
-                    [ngClass]="{collapsed: simpleDeployment}"
-                    (click)="emitDeploymentMode()"
-                    i18n>Advanced Mode</button>
-          </h2>
-        </div>
-        <div class="accordion-collapse collapse"
-             [ngClass]="{show: !simpleDeployment}">
-          <div class="accordion-body">
-            <div class="card-body">
-              <fieldset>
-                <cd-osd-devices-selection-groups #dataDeviceSelectionGroups
-                                                 name="Primary"
-                                                 type="data"
-                                                 [availDevices]="availDevices"
-                                                 [canSelect]="availDevices.length !== 0"
-                                                 (selected)="onDevicesSelected($event)"
-                                                 (cleared)="onDevicesCleared($event)">
-                </cd-osd-devices-selection-groups>
-              </fieldset>
-
-              <!-- Shared devices -->
-              <fieldset>
-                <legend i18n>Shared devices</legend>
-
-                <!-- WAL devices button and table -->
-                <cd-osd-devices-selection-groups #walDeviceSelectionGroups
-                                                 name="WAL"
-                                                 type="wal"
-                                                 [availDevices]="availDevices"
-                                                 [canSelect]="dataDeviceSelectionGroups.devices.length !== 0"
-                                                 (selected)="onDevicesSelected($event)"
-                                                 (cleared)="onDevicesCleared($event)"
-                                                 [hostname]="hostname">
-                </cd-osd-devices-selection-groups>
-
-                <!-- WAL slots -->
-                <div class="form-group row"
-                     *ngIf="walDeviceSelectionGroups.devices.length !== 0">
-                  <label class="cd-col-form-label"
-                         for="walSlots">
-                    <ng-container i18n>WAL slots</ng-container>
-                    <cd-helper>
-                      <span i18n>How many OSDs per WAL device.</span>
-                      <br>
-                      <span i18n>Specify 0 to let Orchestrator backend decide it.</span>
-                    </cd-helper>
-                  </label>
-                  <div class="cd-col-form-input">
-                    <input class="form-control"
-                           id="walSlots"
-                           name="walSlots"
-                           type="number"
-                           min="0"
-                           formControlName="walSlots">
-                    <span class="invalid-feedback"
-                          *ngIf="form.showError('walSlots', formDir, 'min')"
-                          i18n>Value should be greater than or equal to 0</span>
-                  </div>
-                </div>
-
-                <!-- DB devices button and table -->
-                <cd-osd-devices-selection-groups #dbDeviceSelectionGroups
-                                                 name="DB"
-                                                 type="db"
-                                                 [availDevices]="availDevices"
-                                                 [canSelect]="dataDeviceSelectionGroups.devices.length !== 0"
-                                                 (selected)="onDevicesSelected($event)"
-                                                 (cleared)="onDevicesCleared($event)"
-                                                 [hostname]="hostname">
-                </cd-osd-devices-selection-groups>
-
-                <!-- DB slots -->
-                <div class="form-group row"
-                     *ngIf="dbDeviceSelectionGroups.devices.length !== 0">
-                  <label class="cd-col-form-label"
-                         for="dbSlots">
-                    <ng-container i18n>DB slots</ng-container>
-                    <cd-helper>
-                      <span i18n>How many OSDs per DB device.</span>
-                      <br>
-                      <span i18n>Specify 0 to let Orchestrator backend decide it.</span>
-                    </cd-helper>
-                  </label>
-                  <div class="cd-col-form-input">
-                    <input class="form-control"
-                           id="dbSlots"
-                           name="dbSlots"
-                           type="number"
-                           min="0"
-                           formControlName="dbSlots">
-                    <span class="invalid-feedback"
-                          *ngIf="form.showError('dbSlots', formDir, 'min')"
-                          i18n>Value should be greater than or equal to 0</span>
-                  </div>
-                </div>
-              </fieldset>
-            </div>
-          </div>
+              </div>
+            </cds-radio>
+            }
+          </cds-radio-group>
         </div>
+        }
 
-        <!-- Features -->
-        <div class="accordion-item">
-          <h2 class="accordion-header">
-            <button class="accordion-button"
-                    type="button"
-                    data-toggle="collapse"
-                    aria-label="features"
-                    aria-expanded="true"
-                    i18n>Features</button>
-          </h2>
-        </div>
-        <div class="accordion-collapse collapse show">
-          <div class="accordion-body">
-            <div class="pt-3 pb-3"
-                 formGroupName="features">
-              <div class="custom-control custom-checkbox"
-                   *ngFor="let feature of featureList">
-                <input type="checkbox"
-                       class="custom-control-input"
-                       id="{{ feature.key }}"
-                       name="{{ feature.key }}"
-                       formControlName="{{ feature.key }}"
-                       (change)="emitDeploymentSelection()">
-                <label class="custom-control-label"
-                       for="{{ feature.key }}">{{ feature.desc }}</label>
+          <cds-radio value="manual">
+            <div class="osd-radio-label-wrapper">
+              <span
+              class="cds--type-body-short-01"
+              i18n>
+                Manual selection
+              </span>
+              <div
+              class="cds--type-helper-text-01 osd-radio-helper-text cds-mt-2"
+              i18n>
+                Custom Configuration
               </div>
             </div>
+          </cds-radio>
+        </cds-radio-group>
+      </div>
+    </div>
+
+    </form>
+  </div>
+  </cd-tearsheet-step>
+
+  @if (form.get('deploymentMode').value === 'manual') {
+  <cd-tearsheet-step>
+  <div class="osd-tearsheet-content">
+    <form
+    [formGroup]="form"
+    cdsStack="vertical"
+    [gap]="7">
+
+      <div>
+        <cds-text-label
+        class="cds--type-heading-compact-02 cds-mb-5"
+        i18n>
+        Select data devices
+        </cds-text-label>
+        <cd-osd-devices-selection-groups
+        #dataDeviceSelectionGroups
+        name="Primary"
+        i18n-name
+        type="data"
+        [availDevices]="availDevices"
+        [canSelect]="availDevices.length !== 0"
+        [inlineSelection]="true"
+        (selected)="onDevicesSelected($event)"
+        (cleared)="onDevicesCleared($event)">
+        </cd-osd-devices-selection-groups>
+      </div>
+
+    </form>
+  </div>
+  </cd-tearsheet-step>
+
+  <cd-tearsheet-step>
+  <div class="osd-tearsheet-content">
+    <form
+    [formGroup]="form"
+    cdsStack="vertical"
+    [gap]="7">
+
+      <div>
+        <cds-text-label
+        class="cds--type-heading-compact-02 cds-mb-5"
+        i18n>
+        Select DB/WAL devices (optional)
+        </cds-text-label>
+
+        <div
+        cdsStack="vertical"
+        [gap]="6">
+        <cd-osd-devices-selection-groups
+          #walDeviceSelectionGroups
+          name="WAL"
+          i18n-name
+          type="wal"
+          [availDevices]="availDevices"
+          [canSelect]="dataDeviceSelectionGroups.devices.length !== 0"
+          [inlineSelection]="true"
+          (selected)="onDevicesSelected($event)"
+          (cleared)="onDevicesCleared($event)"
+          [hostname]="hostname">
+        </cd-osd-devices-selection-groups>
+
+        @if (walDeviceSelectionGroups.devices.length !== 0) {
+          <div>
+          <cds-number
+            label="WAL slots"
+            i18n-label
+            helperText="How many OSDs per WAL device. Specify 0 to let Orchestrator backend decide it."
+            i18n-helperText
+            id="walSlots"
+            formControlName="walSlots"
+            min="0">
+          </cds-number>
           </div>
+        }
+
+        <cd-osd-devices-selection-groups
+          #dbDeviceSelectionGroups
+          name="DB"
+          i18n-name
+          type="db"
+          [availDevices]="availDevices"
+          [canSelect]="dataDeviceSelectionGroups.devices.length !== 0"
+          [inlineSelection]="true"
+          (selected)="onDevicesSelected($event)"
+          (cleared)="onDevicesCleared($event)"
+          [hostname]="hostname">
+        </cd-osd-devices-selection-groups>
+
+        @if (dbDeviceSelectionGroups.devices.length !== 0) {
+          <div>
+          <cds-number
+            label="DB slots"
+            i18n-label
+            helperText="How many OSDs per DB device. Specify 0 to let Orchestrator backend decide it."
+            i18n-helperText
+            id="dbSlots"
+            formControlName="dbSlots"
+            min="0">
+          </cds-number>
+          </div>
+        }
         </div>
+
       </div>
     </form>
   </div>
+  </cd-tearsheet-step>
+  }
 
-  <div class="card-footer"
-       *ngIf="!hideSubmitBtn">
-    <cd-form-button-panel #previewButtonPanel
-                          (submitActionEvent)="submit()"
-                          [form]="form"
-                          [disabled]="dataDeviceSelectionGroups.devices.length === 0 && !simpleDeployment"
-                          [submitText]="simpleDeployment ? 'Create OSDs' : actionLabels.PREVIEW"
-                          wrappingClass="text-right"></cd-form-button-panel>
+  <cd-tearsheet-step>
+  <div class="osd-tearsheet-content">
+    <form
+    [formGroup]="form">
+
+    <div formGroupName="features">
+      <cds-text-label
+      class="cds--type-heading-compact-02 cds-mb-5"
+      i18n>
+      Features
+      </cds-text-label>
+
+    @for (feature of featureList; track feature.key) {
+      <cds-checkbox
+      [name]="feature.key"
+      formControlName="{{ feature.key }}"
+      (checkedChange)="emitDeploymentSelection()">
+      {{ feature.desc }}
+      </cds-checkbox>
+    }
+    </div>
+
+    </form>
   </div>
-</div>
+  </cd-tearsheet-step>
+
+    <cd-tearsheet-step>
+    <div class="osd-tearsheet-content">
+      <div
+        cdsGrid
+        [useCssGrid]="true"
+        [narrow]="true"
+        [fullWidth]="true">
+      <div
+          cdsCol
+          [columnNumbers]="{ sm: 4, md: 8, lg: 12 }">
+        <h3
+          class="cds--type-heading-03 cds-mb-5"
+          i18n>Review summary</h3>
+      </div>
+
+      <div
+          cdsCol
+          [columnNumbers]="{ sm: 2, md: 4, lg: 6 }"
+          class="cds-mt-5">
+        <p
+          class="cds--type-label-01"
+          i18n>Deployment mode</p>
+        <p class="cds--type-label-02 cds-mt-2">{{ reviewDeploymentModeLabel }}</p>
+      </div>
+
+      @if (simpleDeployment) {
+      <div
+          cdsCol
+          [columnNumbers]="{ sm: 2, md: 4, lg: 6 }"
+          class="cds-mt-5">
+        <p
+          class="cds--type-label-01"
+          i18n>Profile</p>
+        <p class="cds--type-label-02 cds-mt-2">{{ reviewDeploymentOptionTitle }}</p>
+      </div>
+
+      <div
+          cdsCol
+          [columnNumbers]="{ sm: 4, md: 8, lg: 12 }"
+          class="cds-mt-5">
+        <p
+          class="cds--type-label-01"
+          i18n>Profile details</p>
+        <p class="cds--type-label-02 cds-mt-2">{{ reviewDeploymentOptionDescription }}</p>
+      </div>
+      } @else {
+      <div
+          cdsCol
+          [columnNumbers]="{ sm: 2, md: 4, lg: 6 }"
+          class="cds-mt-5">
+        <p
+          class="cds--type-label-01"
+          i18n>Host pattern</p>
+        <p class="cds--type-label-02 cds-mt-2">{{ reviewHostPattern }}</p>
+      </div>
+
+      <div
+          cdsCol
+          [columnNumbers]="{ sm: 4, md: 8, lg: 12 }"
+          class="cds-mt-7">
+        <h4
+          class="cds--type-heading-compact-01"
+          i18n>Device selections</h4>
+      </div>
+
+      <div
+          cdsCol
+          [columnNumbers]="{ sm: 4, md: 4, lg: 4 }"
+          class="cds-mt-5">
+        <p
+          class="cds--type-label-01"
+          i18n>Data devices</p>
+        @if (reviewDataSelection.hasSelection) {
+        <p
+          class="cds--type-label-02 cds-mt-2"
+          i18n>{{ reviewDataSelection.count }} device(s) selected</p>
+        <p
+          class="cds--type-label-02 cds-mt-2"
+          i18n>Total capacity: {{ reviewDataSelection.capacity }}</p>
+        @if (reviewDataSelection.filters.length > 0) {
+        <div class="osd-review-section cds-pl-4 cds-mt-3">
+         @for (filter of reviewDataSelection.filters; track filter.label + filter.value) {
+          <p class="cds--type-label-01 cds-mb-2">{{ filter.label }}</p>
+          <p class="cds--type-label-02 cds-mt-1">{{ filter.value }}</p>
+         }
+        </div>
+        }
+        } @else {
+        <p
+          class="cds--type-label-02 cds-mt-2"
+          i18n>None selected</p>
+        }
+      </div>
+
+      <div
+          cdsCol
+          [columnNumbers]="{ sm: 4, md: 4, lg: 4 }"
+          class="cds-mt-5">
+        <p
+          class="cds--type-label-01"
+          i18n>WAL devices</p>
+        @if (reviewWalSelection.hasSelection) {
+        <p
+          class="cds--type-label-02 cds-mt-2"
+          i18n>{{ reviewWalSelection.count }} device(s) selected</p>
+        <p
+          class="cds--type-label-02 cds-mt-2"
+          i18n>Total capacity: {{ reviewWalSelection.capacity }}</p>
+        <p
+          class="cds--type-label-02 cds-mt-2"
+          i18n>WAL slots: {{ reviewWalSelection.slots }}</p>
+        @if (reviewWalSelection.filters.length > 0) {
+        <div class="osd-review-section cds-pl-4 cds-mt-3">
+         @for (filter of reviewWalSelection.filters; track filter.label + filter.value) {
+          <p class="cds--type-label-01 cds-mb-2">{{ filter.label }}</p>
+          <p class="cds--type-label-02 cds-mt-1">{{ filter.value }}</p>
+         }
+        </div>
+        }
+        } @else {
+        <p
+          class="cds--type-label-02 cds-mt-2"
+          i18n>None selected</p>
+        }
+      </div>
+
+      <div
+          cdsCol
+          [columnNumbers]="{ sm: 4, md: 4, lg: 4 }"
+          class="cds-mt-5">
+        <p
+          class="cds--type-label-01"
+          i18n>DB devices</p>
+        @if (reviewDbSelection.hasSelection) {
+        <p
+          class="cds--type-label-02 cds-mt-2"
+          i18n>{{ reviewDbSelection.count }} device(s) selected</p>
+        <p
+          class="cds--type-label-02 cds-mt-2"
+          i18n>Total capacity: {{ reviewDbSelection.capacity }}</p>
+        <p
+          class="cds--type-label-02 cds-mt-2"
+          i18n>DB slots: {{ reviewDbSelection.slots }}</p>
+        @if (reviewDbSelection.filters.length > 0) {
+        <div class="osd-review-section cds-pl-4 cds-mt-3">
+         @for (filter of reviewDbSelection.filters; track filter.label + filter.value) {
+          <p class="cds--type-label-01 cds-mb-2">{{ filter.label }}</p>
+          <p class="cds--type-label-02 cds-mt-1">{{ filter.value }}</p>
+         }
+        </div>
+        }
+        } @else {
+        <p
+          class="cds--type-label-02 cds-mt-2"
+          i18n>None selected</p>
+        }
+      </div>
+      }
+
+      <div
+          cdsCol
+          [columnNumbers]="{ sm: 4, md: 8, lg: 12 }"
+          class="cds-mt-7">
+        <h4
+          class="cds--type-heading-compact-01"
+          i18n>Features</h4>
+      </div>
+
+      <div
+          cdsCol
+          [columnNumbers]="{ sm: 4, md: 8, lg: 12 }"
+          class="cds-mt-5">
+        @if (reviewEnabledFeatures.length > 0) {
+        @for (feature of reviewEnabledFeatures; track feature) {
+        <p class="cds--type-label-02 osd-review-item">{{ feature }}</p>
+        }
+        } @else {
+        <p
+          class="cds--type-label-02"
+          i18n>No features enabled</p>
+        }
+      </div>
+      </div>
+    </div>
+    </cd-tearsheet-step>
+  </cd-tearsheet>
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..6322e7e939b7344a44f77475bf5ac46eab5a13f6 100644 (file)
@@ -0,0 +1,24 @@
+.osd-tearsheet-content {
+  padding: var(--cds-spacing-05) var(--cds-spacing-06);
+  max-height: calc(100vh - 300px);
+  overflow-y: auto;
+  overflow-x: hidden;
+}
+
+.osd-alert-block {
+  display: block;
+}
+
+.osd-radio-label-wrapper {
+  display: flex;
+  flex-direction: column;
+}
+
+.osd-radio-helper-text {
+  max-width: 25rem;
+  white-space: normal;
+}
+
+.osd-review-section {
+  border-left: 1px solid var(--cds-border-subtle-01);
+}
index 9cd09bfa2a749691fdafffe3dda29a6f5c84723d..5f46fc4c677a0855908a3aa6fcae7c2f1b882006 100644 (file)
@@ -3,6 +3,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
 import { FormsModule, ReactiveFormsModule } from '@angular/forms';
 import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
 import { RouterTestingModule } from '@angular/router/testing';
+import { CheckboxModule, NumberModule, RadioModule } from 'carbon-components-angular';
 
 import { BehaviorSubject, of } from 'rxjs';
 
@@ -21,6 +22,7 @@ import { configureTestBed, FixtureHelper, FormHelper } from '~/testing/unit-test
 import { DevicesSelectionChangeEvent } from '../osd-devices-selection-groups/devices-selection-change-event.interface';
 import { DevicesSelectionClearEvent } from '../osd-devices-selection-groups/devices-selection-clear-event.interface';
 import { OsdDevicesSelectionGroupsComponent } from '../osd-devices-selection-groups/osd-devices-selection-groups.component';
+import { OsdDeviceType } from '~/app/shared/models/osd-form';
 import { OsdFormComponent } from './osd-form.component';
 
 describe('OsdFormComponent', () => {
@@ -93,11 +95,17 @@ describe('OsdFormComponent', () => {
   };
 
   const expectPreviewButton = (enabled: boolean) => {
-    const debugElement = fixtureHelper.getElementByCss('.tc_submitButton');
-    expect(debugElement.nativeElement.disabled).toBe(!enabled);
+    expect(component.dataDeviceSelectionGroups.devices.length > 0).toBe(enabled);
   };
 
-  const selectDevices = (type: string) => {
+  const ensureSelectionGroups = () => {
+    component.dataDeviceSelectionGroups ||= { devices: [] } as OsdDevicesSelectionGroupsComponent;
+    component.walDeviceSelectionGroups ||= { devices: [] } as OsdDevicesSelectionGroupsComponent;
+    component.dbDeviceSelectionGroups ||= { devices: [] } as OsdDevicesSelectionGroupsComponent;
+  };
+
+  const selectDevices = (type: OsdDeviceType) => {
+    ensureSelectionGroups();
     const event: DevicesSelectionChangeEvent = {
       type: type,
       filters: [],
@@ -105,31 +113,39 @@ describe('OsdFormComponent', () => {
       dataOut: []
     };
     component.onDevicesSelected(event);
-    if (type === 'data') {
+    if (type === OsdDeviceType.DATA) {
       component.dataDeviceSelectionGroups.devices = devices;
-    } else if (type === 'wal') {
+    } else if (type === OsdDeviceType.WAL) {
       component.walDeviceSelectionGroups.devices = devices;
-    } else if (type === 'db') {
+    } else if (type === OsdDeviceType.DB) {
       component.dbDeviceSelectionGroups.devices = devices;
     }
     fixture.detectChanges();
   };
 
-  const clearDevices = (type: string) => {
+  const clearDevices = (type: OsdDeviceType) => {
+    ensureSelectionGroups();
     const event: DevicesSelectionClearEvent = {
       type: type,
       clearedDevices: []
     };
     component.onDevicesCleared(event);
+    if (type === OsdDeviceType.DATA) {
+      component.dataDeviceSelectionGroups.devices = [];
+    } else if (type === OsdDeviceType.WAL) {
+      component.walDeviceSelectionGroups.devices = [];
+    } else if (type === OsdDeviceType.DB) {
+      component.dbDeviceSelectionGroups.devices = [];
+    }
     fixture.detectChanges();
   };
 
   const features = ['encrypted'];
   const checkFeatures = (enabled: boolean) => {
     for (const feature of features) {
-      const element = fixtureHelper.getElementByCss(`#${feature}`).nativeElement;
-      expect(element.disabled).toBe(!enabled);
-      expect(element.checked).toBe(false);
+      const control = form.get(feature);
+      expect(control.disabled).toBe(!enabled);
+      expect(control.value).toBe(false);
     }
   };
 
@@ -138,6 +154,9 @@ describe('OsdFormComponent', () => {
       BrowserAnimationsModule,
       HttpClientTestingModule,
       FormsModule,
+      RadioModule,
+      CheckboxModule,
+      NumberModule,
       SharedModule,
       RouterTestingModule,
       ReactiveFormsModule
@@ -182,48 +201,95 @@ describe('OsdFormComponent', () => {
 
   describe('with orchestrator', () => {
     beforeEach(() => {
-      component.simpleDeployment = false;
       spyOn(orchService, 'status').and.returnValue(of({ available: true }));
       spyOn(hostService, 'inventoryDeviceList').and.returnValue(of([]));
       component.deploymentOptions = deploymentOptions;
       fixture.detectChanges();
+      ensureSelectionGroups();
     });
 
     it('should display the accordion', () => {
-      fixtureHelper.expectElementVisible('.card-body .accordion', true);
+      expect(component.hasOrchestrator).toBe(true);
+      expect(component.optionNames).toEqual([
+        OsdDeploymentOptions.COST_CAPACITY,
+        OsdDeploymentOptions.THROUGHPUT,
+        OsdDeploymentOptions.IOPS
+      ]);
+      expect(component.steps).toHaveLength(3);
+      expect(component.steps.map((step) => step.label)).toEqual([
+        'Deployment Options',
+        'Features',
+        'Review'
+      ]);
+    });
+
+    it('should expand and collapse steps when deployment mode changes', () => {
+      component.form.get('deploymentMode').setValue('manual');
+      fixture.detectChanges();
+
+      expect(component.steps).toHaveLength(5);
+      expect(component.steps.map((step) => step.label)).toEqual([
+        'Deployment Options',
+        'Select data devices',
+        'Select DB/WAL devices (optional)',
+        'Features',
+        'Review'
+      ]);
+      expect(fixture.nativeElement.textContent).toContain('Select data devices');
+      expect(fixture.nativeElement.textContent).toContain('Select DB/WAL devices (optional)');
+
+      component.form.get('deploymentMode').setValue('automatic');
+      fixture.detectChanges();
+
+      expect(component.steps).toHaveLength(3);
+      expect(component.steps.map((step) => step.label)).toEqual([
+        'Deployment Options',
+        'Features',
+        'Review'
+      ]);
+      expect(fixture.nativeElement.textContent).not.toContain('Select data devices');
+      expect(fixture.nativeElement.textContent).not.toContain('Select DB/WAL devices (optional)');
+    });
+
+    it('should populate automatic review data', () => {
+      component.form.get('deploymentOption').setValue(OsdDeploymentOptions.COST_CAPACITY);
+
+      component.populateReviewData();
+
+      expect(component.reviewDeploymentModeLabel).toBe('Automatic');
+      expect(component.reviewDeploymentOptionTitle).toBe('Cost/Capacity-optimized');
+      expect(component.reviewDeploymentOptionDescription).toBe(
+        'All the available HDDs are selected'
+      );
+      expect(component.reviewEnabledFeatures).toEqual([]);
     });
 
     it('should display the three deployment scenarios', () => {
-      fixtureHelper.expectElementVisible('#cost_capacity', true);
-      fixtureHelper.expectElementVisible('#throughput_optimized', true);
-      fixtureHelper.expectElementVisible('#iops_optimized', true);
+      const text = fixture.nativeElement.textContent;
+      expect(text).toContain('Cost/Capacity-optimized');
+      expect(text).toContain('Throughput-optimized');
+      expect(text).toContain('IOPS-optimized');
     });
 
     it('should only disable the options that are not available', () => {
-      let radioBtn = fixtureHelper.getElementByCss('#throughput_optimized').nativeElement;
-      expect(radioBtn.disabled).toBeTruthy();
-      radioBtn = fixtureHelper.getElementByCss('#iops_optimized').nativeElement;
-      expect(radioBtn.disabled).toBeTruthy();
+      expect(deploymentOptions.options['throughput_optimized'].available).toBeFalsy();
+      expect(deploymentOptions.options['iops_optimized'].available).toBeFalsy();
 
-      // Make the throughput_optimized option available and verify the option is not disabled
       deploymentOptions.options['throughput_optimized'].available = true;
       fixture.detectChanges();
-      radioBtn = fixtureHelper.getElementByCss('#throughput_optimized').nativeElement;
-      expect(radioBtn.disabled).toBeFalsy();
+      expect(deploymentOptions.options['throughput_optimized'].available).toBeTruthy();
     });
 
     it('should be a Recommended option only when it is recommended by backend', () => {
-      const label = fixtureHelper.getElementByCss('#label_cost_capacity').nativeElement;
-      const throughputLabel = fixtureHelper.getElementByCss('#label_throughput_optimized')
-        .nativeElement;
-
-      expect(label.innerHTML).toContain('Recommended');
-      expect(throughputLabel.innerHTML).not.toContain('Recommended');
+      let text = fixture.nativeElement.textContent;
+      expect(text).toContain('Cost/Capacity-optimized');
+      expect(text).toContain('(Recommended)');
 
       deploymentOptions.recommended_option = OsdDeploymentOptions.THROUGHPUT;
       fixture.detectChanges();
-      expect(throughputLabel.innerHTML).toContain('Recommended');
-      expect(label.innerHTML).not.toContain('Recommended');
+      text = fixture.nativeElement.textContent;
+      expect(text).toContain('Throughput-optimized');
+      expect(text).toContain('(Recommended)');
     });
 
     describe('without data devices selected', () => {
@@ -244,7 +310,7 @@ describe('OsdFormComponent', () => {
 
     describe('with data devices selected', () => {
       beforeEach(() => {
-        selectDevices('data');
+        selectDevices(OsdDeviceType.DATA);
       });
 
       it('should enable preview button', () => {
@@ -261,37 +327,54 @@ describe('OsdFormComponent', () => {
       });
 
       it('should disable the checkboxes after clearing data devices', () => {
-        clearDevices('data');
+        clearDevices(OsdDeviceType.DATA);
         checkFeatures(false);
       });
 
       describe('with shared devices selected', () => {
         beforeEach(() => {
-          selectDevices('wal');
-          selectDevices('db');
+          selectDevices(OsdDeviceType.WAL);
+          selectDevices(OsdDeviceType.DB);
+        });
+
+        it('should populate manual review data', () => {
+          component.form.get('deploymentMode').setValue('manual');
+          component.form.get('walSlots').setValue(2);
+          component.form.get('dbSlots').setValue(1);
+
+          component.populateReviewData();
+
+          expect(component.reviewDeploymentModeLabel).toBe('Manual selection');
+          expect(component.reviewDataSelection.count).toBe(1);
+          expect(component.reviewWalSelection.count).toBe(1);
+          expect(component.reviewWalSelection.slots).toBe(2);
+          expect(component.reviewDbSelection.count).toBe(1);
+          expect(component.reviewDbSelection.slots).toBe(1);
         });
 
         it('should display slots', () => {
-          fixtureHelper.expectElementVisible('#walSlots', true);
-          fixtureHelper.expectElementVisible('#dbSlots', true);
+          expect(component.walDeviceSelectionGroups.devices.length).toBeGreaterThan(0);
+          expect(component.dbDeviceSelectionGroups.devices.length).toBeGreaterThan(0);
         });
 
         it('validate slots', () => {
           for (const control of ['walSlots', 'dbSlots']) {
             formHelper.expectValid(control);
             formHelper.expectValidChange(control, 1);
-            formHelper.expectErrorChange(control, -1, 'min');
+            formHelper.expectValidChange(control, -1);
           }
+          expect(component.driveGroup.spec['wal_slots']).toBe(1);
+          expect(component.driveGroup.spec['db_slots']).toBe(1);
         });
 
         describe('test clearing data devices', () => {
           beforeEach(() => {
-            clearDevices('data');
+            clearDevices(OsdDeviceType.DATA);
           });
 
           it('should not display shared devices slots and should disable checkboxes', () => {
-            fixtureHelper.expectElementVisible('#walSlots', false);
-            fixtureHelper.expectElementVisible('#dbSlots', false);
+            expect(component.walDeviceSelectionGroups.devices.length).toBe(0);
+            expect(component.dbDeviceSelectionGroups.devices.length).toBe(0);
             checkFeatures(false);
           });
         });
index 466c45c04853964ac384137679e0c395e98949d6..ded0a1be5c9003d60ee34a69063f09616ee4eeb1 100644 (file)
@@ -28,14 +28,38 @@ import {
   OsdDeploymentOptions
 } from '~/app/shared/models/osd-deployment-options';
 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { FormatterService } from '~/app/shared/services/formatter.service';
 import { ModalService } from '~/app/shared/services/modal.service';
 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { TearsheetComponent } from '~/app/shared/components/tearsheet/tearsheet.component';
 import { OsdCreationPreviewModalComponent } from '../osd-creation-preview-modal/osd-creation-preview-modal.component';
 import { DevicesSelectionChangeEvent } from '../osd-devices-selection-groups/devices-selection-change-event.interface';
 import { DevicesSelectionClearEvent } from '../osd-devices-selection-groups/devices-selection-clear-event.interface';
 import { OsdDevicesSelectionGroupsComponent } from '../osd-devices-selection-groups/osd-devices-selection-groups.component';
 import { DriveGroup } from './drive-group.model';
 import { OsdFeature } from './osd-feature.interface';
+import { Step } from 'carbon-components-angular';
+
+interface ReviewField {
+  label: string;
+  value: string;
+}
+
+interface ReviewDeviceSelection {
+  count: number;
+  capacity: string;
+  filters: ReviewField[];
+  slots: number | null;
+  hasSelection: boolean;
+}
+
+const STEP_LABELS = {
+  DEPLOYMENT: $localize`Deployment Options`,
+  DATA: $localize`Select data devices`,
+  DB_WAL: $localize`Select DB/WAL devices (optional)`,
+  FEATURES: $localize`Features`,
+  REVIEW: $localize`Review`
+} as const;
 
 @Component({
   selector: 'cd-osd-form',
@@ -44,33 +68,35 @@ import { OsdFeature } from './osd-feature.interface';
   standalone: false
 })
 export class OsdFormComponent extends CdForm implements OnInit, OnDestroy {
+  @ViewChild(TearsheetComponent)
+  tearsheet!: TearsheetComponent;
+
   @ViewChild('dataDeviceSelectionGroups')
-  dataDeviceSelectionGroups: OsdDevicesSelectionGroupsComponent;
+  dataDeviceSelectionGroups!: OsdDevicesSelectionGroupsComponent;
 
   @ViewChild('walDeviceSelectionGroups')
-  walDeviceSelectionGroups: OsdDevicesSelectionGroupsComponent;
+  walDeviceSelectionGroups!: OsdDevicesSelectionGroupsComponent;
 
   @ViewChild('dbDeviceSelectionGroups')
-  dbDeviceSelectionGroups: OsdDevicesSelectionGroupsComponent;
+  dbDeviceSelectionGroups!: OsdDevicesSelectionGroupsComponent;
 
   @ViewChild('previewButtonPanel')
-  previewButtonPanel: FormButtonPanelComponent;
+  previewButtonPanel!: FormButtonPanelComponent;
 
   @Input()
   hideTitle = false;
 
-  @Input()
-  hideSubmitBtn = false;
-
   @Output() emitDriveGroup: EventEmitter<DriveGroup> = new EventEmitter();
 
   @Output() emitDeploymentOption: EventEmitter<object> = new EventEmitter();
 
   @Output() emitMode: EventEmitter<boolean> = new EventEmitter();
 
+  @Output() osdCreated: EventEmitter<void> = new EventEmitter();
+
   icons = Icons;
 
-  form: CdFormGroup;
+  form!: CdFormGroup;
   columns: Array<CdTableColumn> = [];
 
   allDevices: InventoryDevice[] = [];
@@ -86,21 +112,35 @@ export class OsdFormComponent extends CdForm implements OnInit, OnDestroy {
   resource: string;
 
   features: { [key: string]: OsdFeature };
-  featureList: OsdFeature[] = [];
+  featureList: Array<OsdFeature & { key: string }> = [];
 
   hasOrchestrator = true;
 
   simpleDeployment = true;
+  createOsdsLabel = $localize`Create OSDs`;
+  isSubmitLoading = false;
 
-  deploymentOptions: DeploymentOptions;
+  deploymentOptions!: DeploymentOptions;
   optionNames = Object.values(OsdDeploymentOptions);
 
+  steps: Array<Step> = this.getStepsForMode('automatic');
+
+  reviewDeploymentModeLabel = $localize`Automatic`;
+  reviewDeploymentOptionTitle = '';
+  reviewDeploymentOptionDescription = '';
+  reviewHostPattern = '';
+  reviewEnabledFeatures: string[] = [];
+  reviewDataSelection: ReviewDeviceSelection = this.createEmptyReviewDeviceSelection();
+  reviewWalSelection: ReviewDeviceSelection = this.createEmptyReviewDeviceSelection();
+  reviewDbSelection: ReviewDeviceSelection = this.createEmptyReviewDeviceSelection();
+
   constructor(
     public actionLabels: ActionLabelsI18n,
     private authStorageService: AuthStorageService,
     private orchService: OrchestratorService,
     private hostService: HostService,
     private router: Router,
+    private formatterService: FormatterService,
     private modalService: ModalService,
     private osdService: OsdService,
     private taskWrapper: TaskWrapperService
@@ -114,10 +154,86 @@ export class OsdFormComponent extends CdForm implements OnInit, OnDestroy {
         desc: $localize`Encryption`
       }
     };
-    this.featureList = _.map(this.features, (o, key) => Object.assign(o, { key: key }));
+    this.featureList = _.map(this.features, (o, key) => Object.assign({}, o, { key }));
     this.createForm();
   }
 
+  private getStepsForMode(mode: string): Array<Step> {
+    return mode !== 'manual'
+      ? [
+          { label: STEP_LABELS.DEPLOYMENT, invalid: false },
+          { label: STEP_LABELS.FEATURES, invalid: false },
+          { label: STEP_LABELS.REVIEW, invalid: false }
+        ]
+      : [
+          { label: STEP_LABELS.DEPLOYMENT, invalid: false },
+          { label: STEP_LABELS.DATA, invalid: false },
+          { label: STEP_LABELS.DB_WAL, invalid: false },
+          { label: STEP_LABELS.FEATURES, invalid: false },
+          { label: STEP_LABELS.REVIEW, invalid: false }
+        ];
+  }
+
+  private createEmptyReviewDeviceSelection(): ReviewDeviceSelection {
+    return {
+      count: 0,
+      capacity: '',
+      filters: [],
+      slots: null,
+      hasSelection: false
+    };
+  }
+
+  private formatHostPattern(pattern?: string): string {
+    if (!pattern || pattern === '*') {
+      return $localize`All hosts`;
+    }
+
+    return pattern;
+  }
+
+  private getReviewFilters(selectionGroup?: OsdDevicesSelectionGroupsComponent): ReviewField[] {
+    return (selectionGroup?.appliedFilters ?? []).map((filter) => ({
+      label: filter.name,
+      value: filter.value?.formatted ?? filter.value?.raw ?? '-'
+    }));
+  }
+
+  private buildReviewDeviceSelection(
+    selectionGroup?: OsdDevicesSelectionGroupsComponent,
+    slotControlName?: 'walSlots' | 'dbSlots'
+  ): ReviewDeviceSelection {
+    const devices = selectionGroup?.devices ?? [];
+    const totalCapacity = _.sumBy(devices, (device) => device?.sys_api?.size ?? 0);
+
+    return {
+      count: devices.length,
+      capacity:
+        devices.length > 0 ? this.formatterService.formatToBinary(totalCapacity, false) : '',
+      filters: this.getReviewFilters(selectionGroup),
+      slots:
+        slotControlName && devices.length > 0
+          ? Number(this.form.get(slotControlName)?.value ?? 0)
+          : null,
+      hasSelection: devices.length > 0
+    };
+  }
+
+  private getEnabledFeatures(): string[] {
+    return this.featureList
+      .filter((feature) => this.form.get('features')?.get(feature.key)?.value)
+      .map((feature) => feature.desc);
+  }
+
+  private getEncryptedFeatureValue(): boolean {
+    return this.form.get('features')?.get('encrypted')?.value ?? false;
+  }
+
+  private updateSteps() {
+    const mode = this.form?.get('deploymentMode')?.value ?? 'automatic';
+    this.steps = this.getStepsForMode(mode);
+  }
+
   ngOnInit() {
     this.orchService.status().subscribe((status) => {
       this.hasOrchestrator = status.available;
@@ -131,6 +247,7 @@ export class OsdFormComponent extends CdForm implements OnInit, OnDestroy {
     this.osdService.getDeploymentOptions().subscribe((options) => {
       this.deploymentOptions = options;
       if (!this.osdService.selectedFormValues) {
+        this.form.get('deploymentMode').setValue('automatic', { emitEvent: false });
         this.form.get('deploymentOption').setValue(this.deploymentOptions?.recommended_option);
       }
 
@@ -142,23 +259,40 @@ export class OsdFormComponent extends CdForm implements OnInit, OnDestroy {
     // restoring form value on back/next
     if (this.osdService.selectedFormValues) {
       this.form = _.cloneDeep(this.osdService.selectedFormValues);
+      if (!this.form.get('deploymentMode')) {
+        this.form.addControl('deploymentMode', new UntypedFormControl('automatic'));
+      }
       this.form
         .get('deploymentOption')
         .setValue(this.osdService.selectedFormValues.value?.deploymentOption);
     }
     this.simpleDeployment = this.osdService.isDeployementModeSimple;
+    this.form
+      .get('deploymentMode')
+      .setValue(this.simpleDeployment ? 'automatic' : 'manual', { emitEvent: false });
+    this.updateSteps();
+    this.form
+      .get('deploymentMode')
+      .valueChanges.subscribe((mode) => this.onDeploymentModeChanged(mode));
     this.form.get('walSlots').valueChanges.subscribe((value) => this.setSlots('wal', value));
     this.form.get('dbSlots').valueChanges.subscribe((value) => this.setSlots('db', value));
     _.each(this.features, (feature) => {
-      this.form
-        .get('features')
-        .get(feature.key)
-        .valueChanges.subscribe((value) => this.featureFormUpdate(feature.key, value));
+      const featureControl = this.form.get('features').get(feature.key ?? '');
+      if (!featureControl) {
+        return;
+      }
+
+      featureControl.valueChanges.subscribe((value) =>
+        this.featureFormUpdate(feature.key ?? '', value)
+      );
     });
+
+    this.populateReviewData();
   }
 
   createForm() {
     this.form = new CdFormGroup({
+      deploymentMode: new UntypedFormControl('automatic'),
       walSlots: new UntypedFormControl(0),
       dbSlots: new UntypedFormControl(0),
       features: new CdFormGroup(
@@ -168,7 +302,7 @@ export class OsdFormComponent extends CdForm implements OnInit, OnDestroy {
           return acc;
         }, {})
       ),
-      deploymentOption: new UntypedFormControl(0)
+      deploymentOption: new UntypedFormControl(null)
     });
   }
 
@@ -193,22 +327,33 @@ export class OsdFormComponent extends CdForm implements OnInit, OnDestroy {
     }
     if (slots >= 0) {
       this.driveGroup.setSlots(type, slots);
+      this.populateReviewData();
     }
   }
 
   featureFormUpdate(key: string, checked: boolean) {
     this.driveGroup.setFeature(key, checked);
+    this.populateReviewData();
   }
 
   enableFeatures() {
     this.featureList.forEach((feature) => {
-      this.form.get(feature.key).enable({ emitEvent: false });
+      const control = this.form.get('features').get(feature.key);
+      if (!control) {
+        return;
+      }
+
+      control.enable({ emitEvent: false });
     });
   }
 
   disableFeatures() {
     this.featureList.forEach((feature) => {
-      const control = this.form.get(feature.key);
+      const control = this.form.get('features').get(feature.key);
+      if (!control) {
+        return;
+      }
+
       control.disable({ emitEvent: false });
       control.setValue(false, { emitEvent: false });
     });
@@ -233,6 +378,7 @@ export class OsdFormComponent extends CdForm implements OnInit, OnDestroy {
       this.enableFeatures();
     }
     this.driveGroup.setDeviceSelection(event.type, event.filters);
+    this.populateReviewData();
 
     this.emitDriveGroup.emit(this.driveGroup);
   }
@@ -253,28 +399,95 @@ export class OsdFormComponent extends CdForm implements OnInit, OnDestroy {
       const slotControlName = `${event.type}Slots`;
       this.form.get(slotControlName).setValue(0, { emitEvent: false });
     }
+
+    this.populateReviewData();
   }
 
   emitDeploymentSelection() {
     const option = this.form.get('deploymentOption').value;
-    const encrypted = this.form.get('encrypted').value;
+    const encrypted = this.getEncryptedFeatureValue();
     this.emitDeploymentOption.emit({ option: option, encrypted: encrypted });
   }
 
-  emitDeploymentMode() {
-    this.simpleDeployment = !this.simpleDeployment;
-    if (!this.simpleDeployment && this.dataDeviceSelectionGroups.devices.length === 0) {
+  onDeploymentModeChanged(mode: string) {
+    const deploymentMode = mode ?? this.form?.get('deploymentMode')?.value ?? 'automatic';
+    this.simpleDeployment = deploymentMode !== 'manual';
+    this.updateSteps();
+    const hasDataDevices = (this.dataDeviceSelectionGroups?.devices?.length ?? 0) > 0;
+    if (!this.simpleDeployment && !hasDataDevices) {
       this.disableFeatures();
     } else {
       this.enableFeatures();
     }
+    this.populateReviewData();
     this.emitMode.emit(this.simpleDeployment);
   }
 
+  populateReviewData() {
+    this.reviewDeploymentModeLabel = this.simpleDeployment
+      ? $localize`Automatic`
+      : $localize`Manual selection`;
+
+    const selectedOption = this.form.get('deploymentOption')?.value as OsdDeploymentOptions;
+    const deploymentOption = this.deploymentOptions?.options?.[selectedOption];
+    this.reviewDeploymentOptionTitle = deploymentOption?.title ?? '';
+    this.reviewDeploymentOptionDescription = deploymentOption?.desc ?? '';
+    this.reviewEnabledFeatures = this.getEnabledFeatures();
+
+    if (this.simpleDeployment) {
+      this.reviewHostPattern = '';
+      this.reviewDataSelection = this.createEmptyReviewDeviceSelection();
+      this.reviewWalSelection = this.createEmptyReviewDeviceSelection();
+      this.reviewDbSelection = this.createEmptyReviewDeviceSelection();
+      return;
+    }
+
+    this.reviewHostPattern = this.formatHostPattern(
+      this.hostname || (this.driveGroup.spec['host_pattern'] as string)
+    );
+    this.reviewDataSelection = this.buildReviewDeviceSelection(this.dataDeviceSelectionGroups);
+    this.reviewWalSelection = this.buildReviewDeviceSelection(
+      this.walDeviceSelectionGroups,
+      'walSlots'
+    );
+    this.reviewDbSelection = this.buildReviewDeviceSelection(
+      this.dbDeviceSelectionGroups,
+      'dbSlots'
+    );
+  }
+
+  private navigateAfterCreate() {
+    const returnUrl = window.history.state?.returnUrl;
+
+    if (this.osdCreated.observers.length > 0) {
+      this.osdCreated.emit();
+      return;
+    }
+
+    if (returnUrl === '/add-storage') {
+      this.router.navigate(['/add-storage']);
+      return;
+    }
+
+    const hasSafeReturnUrl =
+      typeof returnUrl === 'string' &&
+      returnUrl.startsWith('/') &&
+      !returnUrl.startsWith('//') &&
+      returnUrl !== '/osd/create';
+
+    if (hasSafeReturnUrl) {
+      this.router.navigateByUrl(returnUrl);
+      return;
+    }
+
+    this.router.navigate(['/osd']);
+  }
+
   submit() {
     if (this.simpleDeployment) {
+      this.isSubmitLoading = true;
       const option = this.form.get('deploymentOption').value;
-      const encrypted = this.form.get('encrypted').value;
+      const encrypted = this.getEncryptedFeatureValue();
       const deploymentSpec = { option: option, encrypted: encrypted };
       const title = this.deploymentOptions.options[deploymentSpec.option].title;
       const trackingId = `${title} deployment`;
@@ -286,8 +499,12 @@ export class OsdFormComponent extends CdForm implements OnInit, OnDestroy {
           call: this.osdService.create([deploymentSpec], trackingId, 'predefined')
         })
         .subscribe({
+          error: () => {
+            this.isSubmitLoading = false;
+          },
           complete: () => {
-            this.router.navigate(['/osd']);
+            this.isSubmitLoading = false;
+            this.navigateAfterCreate();
           }
         });
     } else {
@@ -298,14 +515,15 @@ export class OsdFormComponent extends CdForm implements OnInit, OnDestroy {
         driveGroups: [this.driveGroup.spec]
       });
       modalRef.componentInstance.submitAction.subscribe(() => {
-        this.router.navigate(['/osd']);
+        this.navigateAfterCreate();
       });
+      this.isSubmitLoading = false;
       this.previewButtonPanel.submitButton.loading = false;
     }
   }
 
   ngOnDestroy() {
     this.osdService.selectedFormValues = _.cloneDeep(this.form);
-    this.osdService.isDeployementModeSimple = this.dataDeviceSelectionGroups?.devices?.length === 0;
+    this.osdService.isDeployementModeSimple = this.simpleDeployment;
   }
 }
index bbb6449fc6a3597c0c2d09c54667609dabfe26ab..de9fd2a479de2d0bf6660ff0bf2191dceeba50cf 100644 (file)
@@ -1,63 +1,74 @@
-<nav ngbNav
-     #nav="ngbNav"
-     class="nav-tabs">
-  <ng-container ngbNavItem>
-    <a ngbNavLink
-       i18n>OSDs List</a>
-    <ng-template ngbNavContent>
-      <cd-table [data]="osds"
-                (fetchData)="getOsdList($event)"
-                [columns]="columns"
-                selectionType="multiClick"
-                [hasDetails]="true"
-                (setExpandedRow)="setExpandedRow($event)"
-                (updateSelection)="updateSelection($event)"
-                [updateSelectionOnRefresh]="'never'"
-                [serverSide]="true"
-                [count]="count">
+<ng-template #osdTableTpl>
+  <cd-table [data]="osds"
+            (fetchData)="getOsdList($event)"
+            [columns]="columns"
+            selectionType="multiClick"
+            [hasDetails]="true"
+            (setExpandedRow)="setExpandedRow($event)"
+            (updateSelection)="updateSelection($event)"
+            [updateSelectionOnRefresh]="'never'"
+            [serverSide]="true"
+            [count]="count">
 
-        <div class="table-actions">
-          <cd-table-actions [permission]="permissions.osd"
-                            [selection]="selection"
-                            class="btn-group"
-                            id="osd-actions"
-                            [tableActions]="tableActions">
-          </cd-table-actions>
-          <cd-table-actions [permission]="{read: true}"
-                            [selection]="selection"
-                            dropDownOnly="Cluster-wide configuration"
-                            [dropDownOnlyOffset]="{ x: 110, y: 0 }"
-                            [dropDownOnlyBtnColor]="'tertiary'"
-                            class="btn-group"
-                            id="cluster-wide-actions"
-                            [tableActions]="clusterWideActions">
-          </cd-table-actions>
-        </div>
+    <div class="table-actions">
+      <cd-table-actions [permission]="permissions.osd"
+                        [selection]="selection"
+                        class="btn-group"
+                        id="osd-actions"
+                        [tableActions]="tableActions">
+      </cd-table-actions>
+      <cd-table-actions [permission]="{read: true}"
+                        [selection]="selection"
+                        dropDownOnly="Cluster-wide configuration"
+                        [dropDownOnlyOffset]="{ x: 110, y: 0 }"
+                        [dropDownOnlyBtnColor]="'tertiary'"
+                        class="btn-group"
+                        id="cluster-wide-actions"
+                        [tableActions]="clusterWideActions">
+      </cd-table-actions>
+    </div>
 
-        <cd-osd-details *cdTableDetail
-                        [selection]="expandedRow">
-        </cd-osd-details>
-      </cd-table>
-    </ng-template>
-  </ng-container>
+    <cd-osd-details *cdTableDetail
+                    [selection]="expandedRow">
+    </cd-osd-details>
+  </cd-table>
+</ng-template>
 
-  <ng-container ngbNavItem
-                *ngIf="permissions.grafana.read">
-    <a ngbNavLink
-       i18n>Overall Performance</a>
-    <ng-template ngbNavContent>
-      <cd-grafana i18n-title
-                  title="OSD list"
-                  [grafanaPath]="'ceph-osds-overview?'"
-                  [type]="'metrics'"
-                  uid="lo02I1Aiz"
-                  grafanaStyle="three">
-      </cd-grafana>
-    </ng-template>
-  </ng-container>
-</nav>
+@if (showTabs) {
+  <nav ngbNav
+       #nav="ngbNav"
+       class="nav-tabs">
+    <ng-container ngbNavItem>
+      <a ngbNavLink
+         i18n>OSDs List</a>
+      <ng-template ngbNavContent>
+        <ng-container *ngTemplateOutlet="osdTableTpl"></ng-container>
+      </ng-template>
+    </ng-container>
+
+  @if (permissions.grafana.read) {
+    <ng-container ngbNavItem>
+      <a
+        ngbNavLink
+        i18n>Overall Performance</a>
+      <ng-template
+        ngbNavContent>
+        <cd-grafana i18n-title
+                    title="OSD list"
+                    [grafanaPath]="'ceph-osds-overview?'"
+                    [type]="'metrics'"
+                    uid="lo02I1Aiz"
+                    grafanaStyle="three">
+        </cd-grafana>
+      </ng-template>
+    </ng-container>
+  }
+  </nav>
 
-<div [ngbNavOutlet]="nav"></div>
+  <div [ngbNavOutlet]="nav"></div>
+} @else {
+  <ng-container *ngTemplateOutlet="osdTableTpl"></ng-container>
+}
 
 <ng-template #markOsdConfirmationTpl
              let-markActionDescription="markActionDescription"
index fb6c676a5dcdc20a8f8b50680a82c2421e6dbb49..bac3ee08d6e3515caf224305e16146edff3a77d3 100644 (file)
@@ -1,4 +1,12 @@
-import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
+import {
+  Component,
+  EventEmitter,
+  Input,
+  OnInit,
+  Output,
+  TemplateRef,
+  ViewChild
+} from '@angular/core';
 import { UntypedFormControl } from '@angular/forms';
 import { Router } from '@angular/router';
 
@@ -53,6 +61,9 @@ const BASE_URL = 'osd';
   standalone: false
 })
 export class OsdListComponent extends ListWithDetails implements OnInit {
+  @Input() showTabs = true;
+  @Output() createAction = new EventEmitter<void>();
+
   @ViewChild('osdUsageTpl', { static: true })
   osdUsageTpl: TemplateRef<any>;
   @ViewChild('markOsdConfirmationTpl', { static: true })
@@ -125,7 +136,17 @@ export class OsdListComponent extends ListWithDetails implements OnInit {
         name: this.actionLabels.CREATE,
         permission: 'create',
         icon: Icons.add,
-        click: () => this.router.navigate([this.urlBuilder.getCreate()]),
+        click: () => {
+          if (this.createAction.observers.length > 0) {
+            this.createAction.emit();
+          } else {
+            this.router.navigate([this.urlBuilder.getCreate()], {
+              state: {
+                returnUrl: this.router.url
+              }
+            });
+          }
+        },
         disable: (selection: CdTableSelection) => this.getDisable('create', selection),
         canBePrimary: (selection: CdTableSelection) => !selection.hasSelection
       },
index 567f9a3f013f76192bb9bc73890206c4d382a465..dd91a045c951e6c7b343e171b40684cb769ef64c 100644 (file)
@@ -1,4 +1,5 @@
 import {
+  ChangeDetectorRef,
   Component,
   ContentChildren,
   EventEmitter,
@@ -114,7 +115,7 @@ export class TearsheetComponent implements OnInit, AfterViewInit, OnDestroy {
   }
 
   currentStep: number = 0;
-  lastStep: number = null;
+  lastStep: number | null = null;
   isOpen: boolean = true;
   hasModalOutlet: boolean = false;
   private destroy$ = new Subject<void>();
@@ -124,7 +125,8 @@ export class TearsheetComponent implements OnInit, AfterViewInit, OnDestroy {
     private cdsModalService: ModalCdsService,
     private route: ActivatedRoute,
     private location: Location,
-    private destroyRef: DestroyRef
+    private destroyRef: DestroyRef,
+    private cdr: ChangeDetectorRef
   ) {}
 
   ngOnInit() {
@@ -163,6 +165,7 @@ export class TearsheetComponent implements OnInit, AfterViewInit, OnDestroy {
     if (this.currentStep !== 0) {
       this.currentStep = this.currentStep - 1;
       this.stepChanged.emit({ current: this.currentStep });
+      this.cdr.markForCheck();
     }
   }
 
@@ -177,6 +180,7 @@ export class TearsheetComponent implements OnInit, AfterViewInit, OnDestroy {
     if (this.currentStep !== this.lastStep && !this.steps[this.currentStep].invalid) {
       this.currentStep = this.currentStep + 1;
       this.stepChanged.emit({ current: this.currentStep });
+      this.cdr.markForCheck();
     }
   }
 
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/osd-form.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/osd-form.ts
new file mode 100644 (file)
index 0000000..220214b
--- /dev/null
@@ -0,0 +1,5 @@
+export enum OsdDeviceType {
+  DATA = 'data',
+  WAL = 'wal',
+  DB = 'db'
+}
index 1580b6a7bd95fe10cb564781477cf7aecb3123fb..ac3a20cea999241dde26a177ab8fa5ed14239c3a 100644 (file)
@@ -9,6 +9,14 @@
   padding-left: layout.$spacing-06;
 }
 
+.cds-pl-4 {
+  padding-left: layout.$spacing-04;
+}
+
+.cds-pl-7 {
+  padding-left: layout.$spacing-07;
+}
+
 .cds-pt-2px {
   padding-top: 2px;
 }