]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Nvmeof add hosts(Add Initiator) in subsystem details
authorSagar Gopale <sagar.gopale@ibm.com>
Wed, 18 Feb 2026 14:12:28 +0000 (19:42 +0530)
committerSagar Gopale <sagar.gopale@ibm.com>
Thu, 26 Feb 2026 09:35:31 +0000 (15:05 +0530)
Fixes: https://tracker.ceph.com/issues/75006
Signed-off-by: Sagar Gopale <sagar.gopale@ibm.com>
16 files changed:
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-subsystem-view/nvme-subsystem-view.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvme-subsystem-view/nvme-subsystem-view.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-list/nvmeof-initiators-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-form/nvmeof-listeners-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-2/nvmeof-subsystem-step-2.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-2/nvmeof-subsystem-step-2.component.scss
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystem-step-2/nvmeof-subsystem-step-2.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/tearsheet/tearsheet.component.html

index 52677a13ebfdcdd0aa04c79cdde0158631233ba0..3b63c5a2731511d96a063d40c5e8746fa810155a 100644 (file)
@@ -443,6 +443,11 @@ const routes: Routes = [
           {
             path: 'performance',
             component: NvmeofSubsystemPerformanceComponent
+          },
+          {
+            path: `${URLVerbs.ADD}/initiator`,
+            component: NvmeofInitiatorsFormComponent,
+            outlet: 'modal'
           }
         ]
       }
index 254b688413486dbb19366086464d62067380272f..f055b715a85b60ca9d8b0e5dbe57b7545993dda7 100644 (file)
@@ -4,6 +4,7 @@ import { RouterTestingModule } from '@angular/router/testing';
 import { SideNavModule, ThemeModule } from 'carbon-components-angular';
 
 import { NvmeSubsystemViewComponent } from './nvme-subsystem-view.component';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
 
 describe('NvmeSubsystemViewComponent', () => {
   let component: NvmeSubsystemViewComponent;
@@ -13,7 +14,7 @@ describe('NvmeSubsystemViewComponent', () => {
     waitForAsync(() => {
       TestBed.configureTestingModule({
         declarations: [NvmeSubsystemViewComponent],
-        imports: [RouterTestingModule, SideNavModule, ThemeModule],
+        imports: [RouterTestingModule, SideNavModule, ThemeModule, HttpClientTestingModule],
         schemas: [CUSTOM_ELEMENTS_SCHEMA]
       }).compileComponents();
     })
index 2bc7b2f994b2ed7e33328e50e1d131d9818debae..a6183a8acf08e15df27ce476f89307d41f735a93 100644 (file)
-<cd-modal [pageURL]="pageURL">
-  <span class="modal-title"
-        i18n>{{ action | titlecase }} {{ resource | upperFirst }}</span>
-  <ng-container class="modal-content">
-    <form name="initiatorForm"
-          #formDir="ngForm"
-          [formGroup]="initiatorForm"
-          novalidate>
-      <div class="modal-body">
-        <!-- Hosts -->
-        <div class="form-group row">
-          <label class="cd-col-form-label required"
-                 i18n>Hosts
-          </label>
-          <div class="cd-col-form-input">
-            <!-- Add host -->
-            <div class="custom-control custom-checkbox"
-                 formGroupName="addHost">
-              <input type="checkbox"
-                     class="custom-control-input"
-                     id="addHostCheck"
-                     name="addHostCheck"
-                     formControlName="addHostCheck"
-                     (change)="setAddHostCheck()"/>
-              <label class="custom-control-label mb-0"
-                     for="addHostCheck"
-                     i18n>Add host</label>
-              <cd-help-text>
-                <span i18n>Allow specific hosts to run NVMe/TCP commands to the NVMe subsystem.</span>
-              </cd-help-text>
-              <div formArrayName="addedHosts"
-                   *ngIf="initiatorForm.get('addHost.addHostCheck').value"  >
-                <div *ngFor="let host of addedHosts.controls; let hi = index"
-                     class="input-group cd-mb my-1">
-                  <input class="cd-form-control"
-                         type="text"
-                         i18n-placeholder
-                         placeholder="Add host nqn"
-                         [required]="!initiatorForm.getValue('allowAnyHost')"
-                         [formControlName]="hi"/>
-                  <button class="btn btn-light"
-                          type="button"
-                          id="add-button-{{hi}}"
-                          [disabled]="initiatorForm.get('addHost.addedHosts').controls[hi].invalid
-                          || initiatorForm.get('addHost.addedHosts').errors?.duplicate
-                          || initiatorForm.get('addHost.addedHosts').controls.length === 32
-                          || (initiatorForm.get('addHost.addedHosts').controls.length !== 1 && initiatorForm.get('addHost.addedHosts').controls.length !== hi+1)"
-                          (click)="addHost()">
-                    <svg [cdsIcon]="icons.add"
-                         [size]="icons.size16"
-                         ></svg>
-                  </button>
-                  <button class="btn btn-light"
-                          type="button"
-                          id="delete-button-{{hi}}"
-                          [disabled]="addedHosts.controls.length === 1"
-                          (click)="removeHost(hi)">
-                    <svg [cdsIcon]="icons.trash"
-                         [size]="icons.size16"
-                         ></svg>
-                  </button>
-                  <ng-container *ngIf="initiatorForm.get('addHost.addedHosts').controls[hi].invalid
-                                && (initiatorForm.get('addHost.addedHosts').controls[hi].dirty
-                                || initiatorForm.get('addHost.addedHosts').controls[hi].touched)">
-                    <span class="invalid-feedback"
-                          *ngIf="initiatorForm.get('addHost.addedHosts').controls[hi].errors?.required"
-                          i18n>This field is required.</span>
-                    <span class="invalid-feedback"
-                          *ngIf="initiatorForm.get('addHost.addedHosts').controls[hi].errors?.pattern"
-                          i18n>Expected NQN format<br/>&lt;<code>nqn.$year-$month.$reverseDomainName:$utf8-string</code>".&gt; or <br/>&lt;<code>nqn.2014-08.org.nvmexpress:uuid:$UUID-string</code>".&gt;</span>
-                    <span class="invalid-feedback"
-                          *ngIf="initiatorForm.get('addHost.addedHosts').controls[hi].errors?.maxLength"
-                          i18n>An NQN may not be more than 223 bytes in length.</span>
-                  </ng-container>
-                </div>
-                <span class="invalid-feedback"
-                      *ngIf="initiatorForm.get('addHost.addedHosts').errors?.duplicate"
-                      i18n>Duplicate entry detected. Enter a unique value.</span>
-              </div>
-            </div>
-            <!-- Allow any host -->
-            <div class="custom-control custom-checkbox pt-0">
-              <input type="checkbox"
-                     class="custom-control-input"
-                     id="allowAnyHost"
-                     name="allowAnyHost"
-                     formControlName="allowAnyHost"/>
-              <label class="custom-control-label"
-                     for="allowAnyHost"
-                     i18n>Allow any host</label>
-              <cd-alert-panel *ngIf="initiatorForm.getValue('allowAnyHost')"
-                              [showTitle]="false"
-                              type="warning">Allowing any host to connect to the NVMe/TCP gateway may pose security risks.
-              </cd-alert-panel>
-            </div>
-          </div>
-        </div>
-      </div>
-      <div class="modal-footer">
-        <div class="text-right">
-          <cd-form-button-panel (submitActionEvent)="onSubmit()"
-                                [form]="initiatorForm"
-                                [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"></cd-form-button-panel>
-        </div>
-      </div>
-    </form>
-  </ng-container>
-</cd-modal>
+<cd-tearsheet
+  [steps]="steps"
+  [title]="title"
+  [description]="description"
+  (submitRequested)="onSubmit($event)"
+  [isSubmitLoading]="isSubmitLoading"
+  submitButtonLabel="Add"
+  i18n-submitButtonLabel>
+
+  <cd-tearsheet-step>
+    <cd-nvmeof-subsystem-step-two
+      #tearsheetStep
+               modal-primary-focus
+      [group]="group"
+      [existingHosts]="existingHosts"></cd-nvmeof-subsystem-step-two>
+  </cd-tearsheet-step>
+</cd-tearsheet>
index 7df77e1b99d96a6bd43789a176e27d158990dc0b..e01943f65335db19090236fc5e671ad6c7e6825a 100644 (file)
@@ -2,6 +2,9 @@ import { HttpClientTestingModule } from '@angular/common/http/testing';
 import { ReactiveFormsModule } from '@angular/forms';
 import { RouterTestingModule } from '@angular/router/testing';
 import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ActivatedRoute } from '@angular/router';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { of } from 'rxjs';
 
 import { ToastrModule } from 'ngx-toastr';
 
@@ -9,6 +12,7 @@ import { NgbActiveModal, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
 
 import { SharedModule } from '~/app/shared/shared.module';
 import { NvmeofService } from '~/app/shared/api/nvmeof.service';
+import { HOST_TYPE } from '~/app/shared/models/nvmeof';
 
 import { NvmeofInitiatorsFormComponent } from './nvmeof-initiators-form.component';
 
@@ -22,7 +26,20 @@ describe('NvmeofInitiatorsFormComponent', () => {
     spyOn(Date, 'now').and.returnValue(mockTimestamp);
     await TestBed.configureTestingModule({
       declarations: [NvmeofInitiatorsFormComponent],
-      providers: [NgbActiveModal],
+      schemas: [NO_ERRORS_SCHEMA],
+      providers: [
+        NgbActiveModal,
+        {
+          provide: ActivatedRoute,
+          useValue: {
+            queryParams: of({ group: 'test-group' }),
+            params: of({ subsystem_nqn: 'nqn.test' }),
+            parent: {
+              params: of({ subsystem_nqn: 'nqn.test' })
+            }
+          }
+        }
+      ],
       imports: [
         HttpClientTestingModule,
         NgbTypeaheadModule,
@@ -46,15 +63,23 @@ describe('NvmeofInitiatorsFormComponent', () => {
   describe('should test form', () => {
     beforeEach(() => {
       nvmeofService = TestBed.inject(NvmeofService);
-      spyOn(nvmeofService, 'addSubsystemInitiators').and.stub();
+      spyOn(nvmeofService, 'addInitiators').and.stub();
     });
 
     it('should be creating request correctly', () => {
-      const subsystemNQN = 'nqn.2001-07.com.ceph:' + mockTimestamp;
+      const subsystemNQN = 'nqn.test';
       component.subsystemNQN = subsystemNQN;
-      component.onSubmit();
-      expect(nvmeofService.addSubsystemInitiators).toHaveBeenCalledWith(subsystemNQN, {
-        host_nqn: ''
+      component.group = 'test-group';
+
+      const payload: any = {
+        hostType: HOST_TYPE.SPECIFIC,
+        addedHosts: ['host1']
+      };
+
+      component.onSubmit(payload);
+      expect(nvmeofService.addInitiators).toHaveBeenCalledWith(subsystemNQN, {
+        host_nqn: 'host1',
+        gw_group: 'test-group'
       });
     });
   });
index fd221fef2fe13ee6b5ccbf17fdb4985c8fc7a0b1..666e1f50d35251fe4a79849aa1d4b8d423a5606e 100644 (file)
@@ -1,17 +1,11 @@
 import { Component, OnInit } from '@angular/core';
-import { UntypedFormArray, UntypedFormControl, Validators } from '@angular/forms';
-
-import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
-import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
-import { Icons } from '~/app/shared/enum/icons.enum';
-import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
-import { CdValidators } from '~/app/shared/forms/cd-validators';
-import { Permission } from '~/app/shared/models/permissions';
-import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
-import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { Step } from 'carbon-components-angular';
+import { InitiatorRequest, NvmeofService } from '~/app/shared/api/nvmeof.service';
 import { FinishedTask } from '~/app/shared/models/finished-task';
+import { HOST_TYPE, NvmeofSubsystemInitiator } from '~/app/shared/models/nvmeof';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
 import { ActivatedRoute, Router } from '@angular/router';
-import { InitiatorRequest, NvmeofService } from '~/app/shared/api/nvmeof.service';
+import { SubsystemPayload } from '../nvmeof-subsystems-form/nvmeof-subsystems-form.component';
 
 @Component({
   selector: 'cd-nvmeof-initiators-form',
@@ -20,123 +14,82 @@ import { InitiatorRequest, NvmeofService } from '~/app/shared/api/nvmeof.service
   standalone: false
 })
 export class NvmeofInitiatorsFormComponent implements OnInit {
-  icons = Icons;
-  permission: Permission;
-  initiatorForm: CdFormGroup;
-  action: string;
-  resource: string;
-  pageURL: string;
-  remove: boolean = false;
-  subsystemNQN: string;
-  removeHosts: { name: string; value: boolean; id: number }[] = [];
-  group: string;
+  group!: string;
+  subsystemNQN!: string;
+  isSubmitLoading = false;
+  existingHosts: string[] = [];
+
+  steps: Step[] = [
+    {
+      label: $localize`Host access control`,
+      invalid: false
+    }
+  ];
+
+  title = $localize`Add Initiator`;
+  description = $localize`Allow specific hosts to run NVMe/TCP commands to the NVMe subsystem.`;
+  pageURL = 'block/nvmeof/subsystems';
 
   constructor(
-    private authStorageService: AuthStorageService,
-    public actionLabels: ActionLabelsI18n,
     private nvmeofService: NvmeofService,
     private taskWrapperService: TaskWrapperService,
     private router: Router,
-    private route: ActivatedRoute,
-    private formBuilder: CdFormBuilder
-  ) {
-    this.permission = this.authStorageService.getPermissions().nvmeof;
-    this.resource = $localize`Initiator`;
-    this.pageURL = 'block/nvmeof/subsystems';
-  }
-
-  NQN_REGEX = /^nqn\.(19|20)\d\d-(0[1-9]|1[0-2])\.\D{2,3}(\.[A-Za-z0-9-]+)+(:[A-Za-z0-9-\.]+(:[A-Za-z0-9-\.]+)*)$/;
-  NQN_REGEX_UUID = /^nqn\.2014-08\.org\.nvmexpress:uuid:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
-  ALLOW_ALL_HOST = '*';
-
-  customNQNValidator = CdValidators.custom(
-    'pattern',
-    (nqnInput: string) =>
-      !!nqnInput && !(this.NQN_REGEX.test(nqnInput) || this.NQN_REGEX_UUID.test(nqnInput))
-  );
+    private route: ActivatedRoute
+  ) {}
 
   ngOnInit() {
     this.route.queryParams.subscribe((params) => {
       this.group = params?.['group'];
     });
-    this.createForm();
-    this.action = this.actionLabels.ADD;
-    this.route.params.subscribe((params: { subsystem_nqn: string }) => {
-      this.subsystemNQN = params.subsystem_nqn;
+    this.route.parent.params.subscribe((params: any) => {
+      if (params.subsystem_nqn) {
+        this.subsystemNQN = params.subsystem_nqn;
+      }
     });
-  }
-
-  createForm() {
-    this.initiatorForm = new CdFormGroup({
-      allowAnyHost: new UntypedFormControl(false),
-      addHost: new CdFormGroup({
-        addHostCheck: new UntypedFormControl(false),
-        addedHosts: this.formBuilder.array(
-          [],
-          [
-            CdValidators.custom(
-              'duplicate',
-              (hosts: string[]) => !!hosts.length && new Set(hosts)?.size !== hosts.length
-            )
-          ]
-        )
-      })
+    this.route.params.subscribe((params: any) => {
+      if (!this.subsystemNQN && params.subsystem_nqn) {
+        this.subsystemNQN = params.subsystem_nqn;
+      }
+      this.fetchExistingHosts();
     });
   }
 
-  get addedHosts(): UntypedFormArray {
-    return this.initiatorForm.get('addHost.addedHosts') as UntypedFormArray;
-  }
-
-  addHost() {
-    let newHostFormGroup;
-    newHostFormGroup = this.formBuilder.control('', [this.customNQNValidator, Validators.required]);
-    this.addedHosts.push(newHostFormGroup);
-  }
-
-  removeHost(index: number) {
-    this.addedHosts.removeAt(index);
-  }
-
-  setAddHostCheck() {
-    const addHostCheck = this.initiatorForm.get('addHost.addHostCheck').value;
-    if (!addHostCheck) {
-      while (this.addedHosts.length !== 0) {
-        this.addedHosts.removeAt(0);
-      }
-    } else {
-      this.addHost();
-    }
+  fetchExistingHosts() {
+    if (!this.subsystemNQN || !this.group) return;
+    this.nvmeofService
+      .getInitiators(this.subsystemNQN, this.group)
+      .subscribe((response: NvmeofSubsystemInitiator[] | { hosts: NvmeofSubsystemInitiator[] }) => {
+        const initiators = Array.isArray(response) ? response : response?.hosts || [];
+        this.existingHosts = initiators.map((i) => i.nqn);
+      });
   }
 
-  onSubmit() {
-    const component = this;
-    const allowAnyHost: boolean = this.initiatorForm.getValue('allowAnyHost');
-    const hosts: string[] = this.addedHosts.value;
-    let taskUrl = `nvmeof/initiator/${URLVerbs.ADD}`;
+  onSubmit(payload: SubsystemPayload) {
+    this.isSubmitLoading = true;
+    const taskUrl = `nvmeof/initiator/add`;
 
     const request: InitiatorRequest = {
-      host_nqn: hosts.join(','),
+      host_nqn: payload.hostType === HOST_TYPE.ALL ? '*' : payload.addedHosts.join(','),
       gw_group: this.group
     };
-
-    if (allowAnyHost) {
-      hosts.push('*');
-      request['host_nqn'] = hosts.join(',');
-    }
     this.taskWrapperService
       .wrapTaskAroundCall({
         task: new FinishedTask(taskUrl, {
           nqn: this.subsystemNQN
         }),
-        call: this.nvmeofService.addSubsystemInitiators(this.subsystemNQN, request)
+        call: this.nvmeofService.addInitiators(this.subsystemNQN, request)
       })
       .subscribe({
-        error() {
-          component.initiatorForm.setErrors({ cdSubmitButton: true });
+        error: (err) => {
+          this.isSubmitLoading = false;
+          err.preventDefault();
         },
         complete: () => {
-          this.router.navigate([this.pageURL, { outlets: { modal: null } }]);
+          this.isSubmitLoading = false;
+          this.router.navigate([{ outlets: { modal: null } }], {
+            relativeTo: this.route.parent,
+            queryParamsHandling: 'preserve'
+          });
         }
       });
   }
index e4820f735a0c8a70320ca70ea68511e880a67760..2c2aff3cc9f90104f264b60e2b6ee24da9ae7566 100644 (file)
@@ -93,10 +93,10 @@ export class NvmeofInitiatorsListComponent implements OnInit {
         permission: 'create',
         icon: Icons.add,
         click: () =>
-          this.router.navigate(
-            [BASE_URL, { outlets: { modal: [URLVerbs.ADD, this.subsystemNQN, 'initiator'] } }],
-            { queryParams: { group: this.group } }
-          ),
+          this.router.navigate([{ outlets: { modal: [URLVerbs.ADD, 'initiator'] } }], {
+            queryParams: { group: this.group },
+            relativeTo: this.route.parent
+          }),
         canBePrimary: (selection: CdTableSelection) => !selection.hasSelection,
         disable: () => this.hasAllHostsAllowed()
       },
@@ -206,7 +206,7 @@ export class NvmeofInitiatorsListComponent implements OnInit {
             nqn: this.subsystemNQN,
             plural: itemNames.length > 1
           }),
-          call: this.nvmeofService.removeSubsystemInitiators(this.subsystemNQN, {
+          call: this.nvmeofService.removeInitiators(this.subsystemNQN, {
             host_nqn,
             gw_group: this.group
           })
index 33ec8f4228d66be76d60d1884fb3c9cbb3a0a914..7bf74880c17439ed36e702efb12f5676a746a0fe 100644 (file)
@@ -147,7 +147,9 @@ export class NvmeofListenersFormComponent implements OnInit {
           component.listenerForm.setErrors({ cdSubmitButton: true });
         },
         complete: () => {
-          this.router.navigate([this.pageURL, { outlets: { modal: null } }]);
+          this.router.navigate([this.pageURL, { outlets: { modal: null } }], {
+            queryParamsHandling: 'preserve'
+          });
         }
       });
   }
index 4bb3cdc92a23d09f9b5b7a5d29c5282a78918b36..40e4afd55a18784e9e7dd9890c7593b1d4641e23 100644 (file)
@@ -23,6 +23,7 @@
           orientation="vertical">
           <cds-radio
             [value]="HOST_TYPE.ALL"
+            class="cds-mb-2"
             i18n>Allow all hosts</cds-radio>
           <span class="cds--form__helper-text cds-mb-3"
                 i18n>Any host can connect to this subsystem without verification.</span>
           <cd-alert-panel
             title="Caution"
             type="warning"
+            class="cds-mb-3"
             i18n
             i18n-title>
             Allowing all hosts grants access to every initiator on the network. Authentication is not supported in this mode, which may expose the subsystem to unauthorized access.
           </cd-alert-panel>
           }
-          <cds-radio [value]="HOST_TYPE.SPECIFIC">
-            <span i18n>Restrict to specific hosts</span>
+          <cds-radio
+            [value]="HOST_TYPE.SPECIFIC"
+            class="cds-mb-2">
+            <span
+              class="cds-mr-3"
+              i18n>Restrict to specific hosts</span>
             <cds-tag
               type="blue"
               size="sm"
@@ -45,7 +51,7 @@
               Recommended for secure environments
             </cds-tag>
           </cds-radio>
-          <span class="cds--form__helper-text"
+          <span class="cds--form__helper-text cds-mb-3"
                 i18n>Add the specific hosts permitted to connect.</span>
         </cds-radio-group>
       </div>
index 6e350dcc42c8687b635d106cd7b296720b2d932e..3d43dac63101524e2605f297c39ddcb9a050d47a 100644 (file)
@@ -17,6 +17,7 @@ import { TearsheetStep } from '~/app/shared/models/tearsheet-step';
 })
 export class NvmeofSubsystemsStepTwoComponent implements OnInit, TearsheetStep {
   @Input() group!: string;
+  @Input() existingHosts: string[] = [];
   @ViewChild('rightInfluencer', { static: true })
   rightInfluencer?: TemplateRef<any>;
   formGroup: CdFormGroup;
@@ -37,6 +38,9 @@ export class NvmeofSubsystemsStepTwoComponent implements OnInit, TearsheetStep {
 
   ngOnInit() {
     this.createForm();
+    this.formGroup.get('hostType').valueChanges.subscribe(() => {
+      this.formGroup.get('hostname').updateValueAndValidity();
+    });
   }
 
   isValidNQN = CdValidators.custom(
@@ -46,7 +50,10 @@ export class NvmeofSubsystemsStepTwoComponent implements OnInit, TearsheetStep {
 
   isDuplicate = CdValidators.custom(
     'duplicate',
-    (input: string) => !!input && this.formGroup?.get('addedHosts')?.value.includes(input)
+    (input: string) =>
+      !!input &&
+      (this.formGroup?.get('addedHosts')?.value.includes(input) ||
+        this.existingHosts.includes(input))
   );
 
   isRequired = CdValidators.custom(
index 78aae18c0f9c33bf221deaf01c85a7cb784a2a54..4c7e3d6c3f0f91bbaa80e772b0eec839826389fe 100644 (file)
@@ -73,7 +73,7 @@ describe('NvmeofSubsystemsFormComponent', () => {
     beforeEach(() => {
       nvmeofService = TestBed.inject(NvmeofService);
       spyOn(nvmeofService, 'createSubsystem').and.returnValue(of({}));
-      spyOn(nvmeofService, 'addSubsystemInitiators').and.returnValue(of({}));
+      spyOn(nvmeofService, 'addInitiators').and.returnValue(of({}));
     });
 
     it('should be creating request correctly', () => {
@@ -100,7 +100,7 @@ describe('NvmeofSubsystemsFormComponent', () => {
       component.group = mockGroupName;
       component.onSubmit(payload);
 
-      expect(nvmeofService.addSubsystemInitiators).toHaveBeenCalledWith('test-nqn.default', {
+      expect(nvmeofService.addInitiators).toHaveBeenCalledWith('test-nqn.default', {
         host_nqn: '*',
         gw_group: mockGroupName
       });
index 0a9e6384804760509263e36e39396d00e267a576..4012f8ec4a7b359c66533f7f26d31e1f2525a7cc 100644 (file)
@@ -96,10 +96,7 @@ export class NvmeofSubsystemsFormComponent implements OnInit {
               {
                 step: this.steps[1].label,
                 call: () =>
-                  this.nvmeofService.addSubsystemInitiators(
-                    `${payload.nqn}.${this.group}`,
-                    initiatorRequest
-                  )
+                  this.nvmeofService.addInitiators(`${payload.nqn}.${this.group}`, initiatorRequest)
               }
             ],
             stepResults
index 44cc2b4db8d50cb6202f53bbc49f19c6899e8a72..ede904c2f646c99d4a0f6b91c8c53643809844b6 100755 (executable)
@@ -198,13 +198,13 @@ describe('NvmeofService', () => {
       );
       expect(req.request.method).toBe('GET');
     });
-    it('should call addSubsystemInitiators', () => {
-      service.addSubsystemInitiators(mockNQN, request).subscribe();
+    it('should call addInitiators', () => {
+      service.addInitiators(mockNQN, request).subscribe();
       const req = httpTesting.expectOne(`${UI_API_PATH}/subsystem/${mockNQN}/host`);
       expect(req.request.method).toBe('POST');
     });
-    it('should call removeSubsystemInitiators', () => {
-      service.removeSubsystemInitiators(mockNQN, request).subscribe();
+    it('should call removeInitiators', () => {
+      service.removeInitiators(mockNQN, request).subscribe();
       const req = httpTesting.expectOne(
         `${UI_API_PATH}/subsystem/${mockNQN}/host/${request.host_nqn}/${mockGroupName}`
       );
index 920e78c417a75956735dfbf097b82504c54ab1f0..12641a0f2784d8a784bbe0be91d6381810911a72 100644 (file)
@@ -182,7 +182,7 @@ export class NvmeofService {
     return this.http.get(`${API_PATH}/subsystem/${subsystemNQN}/host?gw_group=${group}`);
   }
 
-  addSubsystemInitiators(subsystemNQN: string, request: InitiatorRequest) {
+  addInitiators(subsystemNQN: string, request: InitiatorRequest) {
     return this.http.post(`${UI_API_PATH}/subsystem/${subsystemNQN}/host`, request, {
       observe: 'response'
     });
@@ -204,7 +204,7 @@ export class NvmeofService {
     });
   }
 
-  removeSubsystemInitiators(subsystemNQN: string, request: InitiatorRequest) {
+  removeInitiators(subsystemNQN: string, request: InitiatorRequest) {
     return this.http.delete(
       `${UI_API_PATH}/subsystem/${subsystemNQN}/host/${request.host_nqn}/${request.gw_group}`,
       {
@@ -213,15 +213,6 @@ export class NvmeofService {
     );
   }
 
-  removeNamespaceInitiators(nsid: string, request: NamespaceInitiatorRequest) {
-    return this.http.delete(
-      `${UI_API_PATH}/namespace/${nsid}/host/${request.subsystem_nqn}/${request.host_nqn}/${request.gw_group}`,
-      {
-        observe: 'response'
-      }
-    );
-  }
-
   // Listeners
   listListeners(subsystemNQN: string, group: string) {
     return this.http.get(`${API_PATH}/subsystem/${subsystemNQN}/listener?gw_group=${group}`);
index 4b49eda01373a62c837591d94877bdaa5c939509..1c4f562d14730fd1fa6452e254269be162066617 100644 (file)
                   (click)="closeTearsheet()"
                   size="xl"
                   i18n>Cancel</button>
+          @if (steps.length > 1) {
           <button cdsButton="secondary"
                   size="xl"
                   [disabled]="currentStep === 0"
                   (click)="onPrevious()"
                   i18n>Previous</button>
+          }
           @if (currentStep === lastStep) {
           <button cdsButton="primary"
                   size="xl"
@@ -96,6 +98,7 @@
            [useCssGrid]="true"
            [fullWidth]="true">
     <!-- Tearsheet Left Influencer-->
+    @if (steps.length > 1) {
     <div cdsCol
          [columnNumbers]="{lg: 3, md: 3, sm: 3}"
          class="tearsheet-left-influencer">
         (stepSelected)="onStepSelect($event)">
       </cds-progress-indicator>
     </div>
+    }
     <div cdsCol
-         [columnNumbers]="{lg: 13, md: 13, sm: 13}"
+         [columnNumbers]="steps.length > 1 ? {lg: 13, md: 13, sm: 13} : {lg: 16, md: 16, sm: 16}"
          class="tearsheet-main">
       @if (showRightInfluencer) {
       <!-- Tearsheet content with right influencer  -->
                 (click)="closeTearsheet()"
                 size="xl"
                 i18n>Cancel</button>
+        @if (steps.length > 1) {
         <button cdsButton="secondary"
                 size="xl"
                 [disabled]="currentStep === 0"
                 (click)="onPrevious()"
                 i18n>Previous</button>
+        }
         @if (currentStep === lastStep) {
         <button cdsButton="primary"
                 size="xl"