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';
FileUploaderModule,
RadioModule,
TilesModule,
- LayerModule
+ LayerModule,
+ AccordionModule
],
declarations: [
MonitorComponent,
<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"
@Output() skipStep = new EventEmitter<void>();
formGroup: CdFormGroup;
+ showForm = false;
ngOnInit() {
this.formGroup = new CdFormGroup({
this.skipStep.emit();
}
+ onOsdCreated() {
+ this.showForm = false;
+ this.formGroup.patchValue({ skipped: true });
+ }
+
+ onCreateAction() {
+ this.showForm = true;
+ }
+
setDriveGroup(driveGroup: DriveGroup) {
this.formGroup.patchValue({ driveGroup });
}
-<!-- 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>
.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%;
+ }
+}
@Input() canSelect: boolean;
+ @Input() inlineSelection = false;
+
@Output()
selected = new EventEmitter<DevicesSelectionChangeEvent>();
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`,
}
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;
clearedDevices: [...this.devices]
};
this.devices = [];
+ this.inlineFilteredDevices = [];
+ this.inlineCapacity = 0;
+ this.canInlineSubmit = false;
+ this.inlineFilterEvent = undefined;
this.cleared.emit(event);
}
}
-<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>
+.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);
+}
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';
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', () => {
};
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: [],
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);
}
};
BrowserAnimationsModule,
HttpClientTestingModule,
FormsModule,
+ RadioModule,
+ CheckboxModule,
+ NumberModule,
SharedModule,
RouterTestingModule,
ReactiveFormsModule
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', () => {
describe('with data devices selected', () => {
beforeEach(() => {
- selectDevices('data');
+ selectDevices(OsdDeviceType.DATA);
});
it('should enable preview button', () => {
});
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);
});
});
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',
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[] = [];
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
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;
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);
}
// 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(
return acc;
}, {})
),
- deploymentOption: new UntypedFormControl(0)
+ deploymentOption: new UntypedFormControl(null)
});
}
}
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 });
});
this.enableFeatures();
}
this.driveGroup.setDeviceSelection(event.type, event.filters);
+ this.populateReviewData();
this.emitDriveGroup.emit(this.driveGroup);
}
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`;
call: this.osdService.create([deploymentSpec], trackingId, 'predefined')
})
.subscribe({
+ error: () => {
+ this.isSubmitLoading = false;
+ },
complete: () => {
- this.router.navigate(['/osd']);
+ this.isSubmitLoading = false;
+ this.navigateAfterCreate();
}
});
} else {
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;
}
}
-<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"
-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';
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 })
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
},
import {
+ ChangeDetectorRef,
Component,
ContentChildren,
EventEmitter,
}
currentStep: number = 0;
- lastStep: number = null;
+ lastStep: number | null = null;
isOpen: boolean = true;
hasModalOutlet: boolean = false;
private destroy$ = new Subject<void>();
private cdsModalService: ModalCdsService,
private route: ActivatedRoute,
private location: Location,
- private destroyRef: DestroyRef
+ private destroyRef: DestroyRef,
+ private cdr: ChangeDetectorRef
) {}
ngOnInit() {
if (this.currentStep !== 0) {
this.currentStep = this.currentStep - 1;
this.stepChanged.emit({ current: this.currentStep });
+ this.cdr.markForCheck();
}
}
if (this.currentStep !== this.lastStep && !this.steps[this.currentStep].invalid) {
this.currentStep = this.currentStep + 1;
this.stepChanged.emit({ current: this.currentStep });
+ this.cdr.markForCheck();
}
}
--- /dev/null
+export enum OsdDeviceType {
+ DATA = 'data',
+ WAL = 'wal',
+ DB = 'db'
+}
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;
}