]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Add iSCSI target edit UI 26367/head
authorTiago Melo <tmelo@suse.com>
Thu, 7 Feb 2019 15:38:21 +0000 (15:38 +0000)
committerTiago Melo <tmelo@suse.com>
Tue, 12 Feb 2019 14:09:37 +0000 (14:09 +0000)
Fixes: http://tracker.ceph.com/issues/38014
Signed-off-by: Tiago Melo <tmelo@suse.com>
src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-list/iscsi-target-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-list/iscsi-target-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/iscsi.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/iscsi.service.ts
src/pybind/mgr/dashboard/frontend/src/locale/messages.xlf

index 908e398398c6a4f011bdcaae40d64f8313c2bd28..7b9e81b578059d10c229df4666487837077046c5 100644 (file)
@@ -200,7 +200,12 @@ const routes: Routes = [
             data: { breadcrumbs: 'Targets' },
             children: [
               { path: '', component: IscsiTargetListComponent },
-              { path: 'add', component: IscsiTargetFormComponent, data: { breadcrumbs: 'Add' } }
+              { path: 'add', component: IscsiTargetFormComponent, data: { breadcrumbs: 'Add' } },
+              {
+                path: 'edit/:target_iqn',
+                component: IscsiTargetFormComponent,
+                data: { breadcrumbs: 'Edit' }
+              }
             ]
           }
         ]
index 00b7b78eeccbeadf3c38e0ba6bb559113a305bf0..750173873ae7fcfba678f188894ff3299bad0302 100644 (file)
@@ -8,7 +8,7 @@
     <div class="panel panel-default">
       <div class="panel-heading">
         <h3 class="panel-title"
-            i18n>Create target</h3>
+            i18n>{isEdit, select, 1 {Edit} other {Add}} target</h3>
       </div>
 
       <div class="panel-body">
           <div class="col-sm-9"
                formArrayName="initiators">
             <div class="panel panel-default"
-                 *ngFor="let initiator of initiators.controls; let i = index"
-                 [formGroupName]="i">
+                 *ngFor="let initiator of initiators.controls; let ii = index"
+                 [formGroupName]="ii">
               <div class="panel-heading">
                 <ng-container i18n>Initiator</ng-container>: {{ initiator.getValue('client_iqn') }}
                 <button type="button"
                         class="close"
-                        (click)="removeInitiator(i)">
+                        (click)="removeInitiator(ii)">
                   <i class="fa fa-remove fa-fw"></i>
                 </button>
               </div>
                          for="luns"
                          i18n>Images</label>
                   <div class="col-sm-9">
-                    <ng-container *ngFor="let image of initiator.getValue('luns'); let i = index">
+                    <ng-container *ngFor="let image of initiator.getValue('luns'); let li = index">
                       <div class="input-group cd-mb">
                         <input class="form-control"
                                type="text"
                         <span class="input-group-btn">
                           <button class="btn btn-default"
                                   type="button"
-                                  (click)="initiator.getValue('luns').splice(i, 1)">
+                                  (click)="removeInitiatorImage(initiator, li, ii, image)">
                             <i class="fa fa-remove fa-fw"
                                aria-hidden="true"></i>
                           </button>
                          *ngIf="!initiator.getValue('cdIsInGroup')">
                       <div class="col-md-12">
                         <cd-select [data]="initiator.getValue('luns')"
-                                   [options]="imagesInitiatorSelections"
+                                   [options]="imagesInitiatorSelections[ii]"
                                    [messages]="messages.initiatorImage"
                                    elemClass="btn btn-default pull-right">
                           <i class="fa fa-fw fa-plus"></i>
                       *ngIf="initiators.controls.length === 0"
                       i18n>No items added.</span>
 
-                <button (click)="addInitiator()"
+                <button (click)="addInitiator(); false"
                         class="btn btn-default pull-right">
                   <i class="fa fa-fw fa-plus"></i>
                   <ng-container i18n>Add initiator</ng-container>
           <div class="col-sm-9"
                formArrayName="groups">
             <div class="panel panel-default"
-                 *ngFor="let group of groups.controls; let i = index"
-                 [formGroupName]="i">
+                 *ngFor="let group of groups.controls; let gi = index"
+                 [formGroupName]="gi">
               <div class="panel-heading">
                 <ng-container i18n>Group</ng-container>: {{ group.getValue('group_id') }}
                 <button type="button"
                         class="close"
-                        (click)="groups.removeAt(i)">
+                        (click)="groups.removeAt(gi)">
                   <i class="fa fa-remove fa-fw"></i>
                 </button>
               </div>
                         <span class="input-group-btn">
                           <button class="btn btn-default"
                                   type="button"
-                                  (click)="removeGroupInitiator(group, i)">
+                                  (click)="removeGroupInitiator(group, i, gi)">
                             <i class="fa fa-remove fa-fw"
                                aria-hidden="true"></i>
                           </button>
                     <div class="row">
                       <div class="col-md-12">
                         <cd-select [data]="group.getValue('members')"
-                                   [options]="groupMembersSelections"
+                                   [options]="groupMembersSelections[gi]"
                                    [messages]="messages.groupInitiator"
                                    (selection)="onGroupMemberSelection($event)"
                                    elemClass="btn btn-default pull-right">
                         <span class="input-group-btn">
                           <button class="btn btn-default"
                                   type="button"
-                                  (click)="group.getValue('disks').splice(i, 1)">
+                                  (click)="removeGroupDisk(group, i, gi)">
                             <i class="fa fa-remove fa-fw"
                                aria-hidden="true"></i>
                           </button>
                     <div class="row">
                       <div class="col-md-12">
                         <cd-select [data]="group.getValue('disks')"
-                                   [options]="groupDiskSelections"
+                                   [options]="groupDiskSelections[gi]"
                                    [messages]="messages.initiatorImage"
                                    elemClass="btn btn-default pull-right">
                           <i class="fa fa-fw fa-plus"></i>
                       *ngIf="groups.controls.length === 0"
                       i18n>No items added.</span>
 
-                <button (click)="addGroup()"
+                <button (click)="addGroup(); false"
                         class="btn btn-default pull-right">
                   <i class="fa fa-fw fa-plus"></i>
                   <ng-container i18n>Add group</ng-container>
           <cd-submit-button [form]="formDir"
                             type="button"
                             (submitAction)="submit()"
-                            i18n>Create target</cd-submit-button>
+                            i18n>Submit</cd-submit-button>
 
           <button type="button"
                   class="btn btn-sm btn-default"
index faf35d0295c7bcbfaa570e0fab6760e3c27cfa03..f5f820f5e8480858c71e8305c151d8d9577cacee 100644 (file)
@@ -1,10 +1,12 @@
 import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
 import { ComponentFixture, TestBed } from '@angular/core/testing';
 import { ReactiveFormsModule } from '@angular/forms';
+import { ActivatedRoute } from '@angular/router';
 import { RouterTestingModule } from '@angular/router/testing';
 
 import { ToastModule } from 'ng2-toastr';
 
+import { ActivatedRouteStub } from '../../../../testing/activated-route-stub';
 import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper';
 import { SharedModule } from '../../../shared/shared.module';
 import { IscsiTargetFormComponent } from './iscsi-target-form.component';
@@ -13,6 +15,7 @@ describe('IscsiTargetFormComponent', () => {
   let component: IscsiTargetFormComponent;
   let fixture: ComponentFixture<IscsiTargetFormComponent>;
   let httpTesting: HttpTestingController;
+  let activatedRoute: ActivatedRouteStub;
 
   const SETTINGS = {
     config: { minimum_gateways: 2 },
@@ -115,7 +118,13 @@ describe('IscsiTargetFormComponent', () => {
         RouterTestingModule,
         ToastModule.forRoot()
       ],
-      providers: [i18nProviders]
+      providers: [
+        i18nProviders,
+        {
+          provide: ActivatedRoute,
+          useValue: new ActivatedRouteStub({ target_iqn: undefined })
+        }
+      ]
     },
     true
   );
@@ -124,6 +133,7 @@ describe('IscsiTargetFormComponent', () => {
     fixture = TestBed.createComponent(IscsiTargetFormComponent);
     component = fixture.componentInstance;
     httpTesting = TestBed.get(HttpTestingController);
+    activatedRoute = TestBed.get(ActivatedRoute);
     fixture.detectChanges();
 
     httpTesting.expectOne('ui-api/iscsi/settings').flush(SETTINGS);
@@ -165,18 +175,8 @@ describe('IscsiTargetFormComponent', () => {
   });
 
   it('should prepare data when selecting an image', () => {
-    expect(component.imagesInitiatorSelections).toEqual([]);
-    expect(component.groupDiskSelections).toEqual([]);
     expect(component.imagesSettings).toEqual({});
-
     component.onImageSelection({ option: { name: 'rbd/disk_1', selected: true } });
-
-    expect(component.imagesInitiatorSelections).toEqual([
-      { description: '', name: 'rbd/disk_1', selected: false }
-    ]);
-    expect(component.groupDiskSelections).toEqual([
-      { description: '', name: 'rbd/disk_1', selected: false }
-    ]);
     expect(component.imagesSettings).toEqual({ 'rbd/disk_1': {} });
   });
 
@@ -197,13 +197,15 @@ describe('IscsiTargetFormComponent', () => {
     component.onImageSelection({ option: { name: 'rbd/disk_1', selected: false } });
 
     expect(component.groups.controls[0].value).toEqual({ disks: [], group_id: 'foo', members: [] });
-    expect(component.imagesInitiatorSelections).toEqual([]);
-    expect(component.groupDiskSelections).toEqual([]);
     expect(component.imagesSettings).toEqual({ 'rbd/disk_1': {} });
   });
 
   describe('should test initiators', () => {
     beforeEach(() => {
+      component.targetForm.patchValue({ disks: ['rbd/disk_1'] });
+      component.addGroup().patchValue({ name: 'group_1' });
+      component.onImageSelection({ option: { name: 'rbd/disk_1', selected: true } });
+
       component.addInitiator();
       component.initiators.controls[0].patchValue({
         client_iqn: 'iqn.initiator'
@@ -219,14 +221,17 @@ describe('IscsiTargetFormComponent', () => {
         client_iqn: 'iqn.initiator',
         luns: []
       });
+      expect(component.imagesInitiatorSelections).toEqual([
+        [{ description: '', name: 'rbd/disk_1', selected: false }]
+      ]);
       expect(component.groupMembersSelections).toEqual([
-        { description: '', name: 'iqn.initiator', selected: false }
+        [{ description: '', name: 'iqn.initiator', selected: false }]
       ]);
     });
 
     it('should update data when changing an initiator name', () => {
       expect(component.groupMembersSelections).toEqual([
-        { description: '', name: 'iqn.initiator', selected: false }
+        [{ description: '', name: 'iqn.initiator', selected: false }]
       ]);
 
       component.initiators.controls[0].patchValue({
@@ -235,12 +240,11 @@ describe('IscsiTargetFormComponent', () => {
       component.updatedInitiatorSelector();
 
       expect(component.groupMembersSelections).toEqual([
-        { description: '', name: 'iqn.initiator_new', selected: false }
+        [{ description: '', name: 'iqn.initiator_new', selected: false }]
       ]);
     });
 
     it('should clean data when removing an initiator', () => {
-      component.addGroup();
       component.groups.controls[0].patchValue({
         group_id: 'foo',
         members: ['iqn.initiator']
@@ -259,7 +263,8 @@ describe('IscsiTargetFormComponent', () => {
         group_id: 'foo',
         members: []
       });
-      expect(component.groupMembersSelections).toEqual([]);
+      expect(component.groupMembersSelections).toEqual([[]]);
+      expect(component.imagesInitiatorSelections).toEqual([]);
     });
 
     it('should remove images in the initiator when added in a group', () => {
@@ -294,40 +299,78 @@ describe('IscsiTargetFormComponent', () => {
     });
   });
 
-  it('should generate the request data', () => {
-    component.onImageSelection({ option: { name: 'rbd/disk_1', selected: true } });
-    component.portals.setValue(['node1:192.168.100.201', 'node2:192.168.100.202']);
-    component.addInitiator();
-    component.initiators.controls[0].patchValue({
-      client_iqn: 'iqn.initiator'
-    });
-    component.addGroup();
-    component.groups.controls[0].patchValue({
-      group_id: 'foo',
-      members: ['iqn.initiator'],
-      disks: ['rbd/disk_1']
+  describe('should submit request', () => {
+    beforeEach(() => {
+      component.targetForm.patchValue({ disks: ['rbd/disk_1'] });
+      component.onImageSelection({ option: { name: 'rbd/disk_1', selected: true } });
+      component.portals.setValue(['node1:192.168.100.201', 'node2:192.168.100.202']);
+      component.addInitiator().patchValue({
+        client_iqn: 'iqn.initiator'
+      });
+      component.addGroup().patchValue({
+        group_id: 'foo',
+        members: ['iqn.initiator'],
+        disks: ['rbd/disk_1']
+      });
     });
 
-    component.submit();
+    it('should call update', () => {
+      activatedRoute.setParams({ target_iqn: 'iqn.iscsi' });
+      component.isEdit = true;
+      component.target_iqn = 'iqn.iscsi';
+
+      component.submit();
+
+      const req = httpTesting.expectOne('api/iscsi/target/iqn.iscsi');
+      expect(req.request.method).toBe('PUT');
+      expect(req.request.body).toEqual({
+        clients: [
+          {
+            auth: { mutual_password: null, mutual_user: null, password: null, user: null },
+            cdIsInGroup: false,
+            client_iqn: 'iqn.initiator',
+            luns: []
+          }
+        ],
+        disks: [{ controls: {}, image: 'disk_1', pool: 'rbd' }],
+        groups: [
+          { disks: [{ image: 'disk_1', pool: 'rbd' }], group_id: 'foo', members: ['iqn.initiator'] }
+        ],
+        new_target_iqn: component.targetForm.value.target_iqn,
+        portals: [
+          { host: 'node1', ip: '192.168.100.201' },
+          { host: 'node2', ip: '192.168.100.202' }
+        ],
+        target_controls: {},
+        target_iqn: component.target_iqn
+      });
+    });
 
-    const req = httpTesting.expectOne('api/iscsi/target');
-    expect(req.request.method).toBe('POST');
-    expect(req.request.body).toEqual({
-      clients: [
-        {
-          auth: { mutual_password: null, mutual_user: null, password: null, user: null },
-          cdIsInGroup: false,
-          client_iqn: 'iqn.initiator',
-          luns: []
-        }
-      ],
-      disks: [],
-      groups: [
-        { disks: [{ image: 'disk_1', pool: 'rbd' }], group_id: 'foo', members: ['iqn.initiator'] }
-      ],
-      portals: [{ host: 'node1', ip: '192.168.100.201' }, { host: 'node2', ip: '192.168.100.202' }],
-      target_controls: {},
-      target_iqn: component.targetForm.value.target_iqn
+    it('should call create', () => {
+      component.submit();
+
+      const req = httpTesting.expectOne('api/iscsi/target');
+      expect(req.request.method).toBe('POST');
+      expect(req.request.body).toEqual({
+        clients: [
+          {
+            auth: { mutual_password: null, mutual_user: null, password: null, user: null },
+            cdIsInGroup: false,
+            client_iqn: 'iqn.initiator',
+            luns: []
+          }
+        ],
+        disks: [{ controls: {}, image: 'disk_1', pool: 'rbd' }],
+        groups: [
+          { disks: [{ image: 'disk_1', pool: 'rbd' }], group_id: 'foo', members: ['iqn.initiator'] }
+        ],
+        portals: [
+          { host: 'node1', ip: '192.168.100.201' },
+          { host: 'node2', ip: '192.168.100.202' }
+        ],
+        target_controls: {},
+        target_iqn: component.targetForm.value.target_iqn
+      });
     });
   });
 });
index fd12c6e60584474f69836850cfad990b2a61ff5d..ee7205018fff61c643d99c8c7aa21178ea4cbfbb 100644 (file)
@@ -1,6 +1,6 @@
-import { Component } from '@angular/core';
+import { Component, OnInit } from '@angular/core';
 import { FormArray, FormControl, Validators } from '@angular/forms';
-import { Router } from '@angular/router';
+import { ActivatedRoute, Router } from '@angular/router';
 
 import { I18n } from '@ngx-translate/i18n-polyfill';
 import * as _ from 'lodash';
@@ -23,19 +23,24 @@ import { IscsiTargetIqnSettingsModalComponent } from '../iscsi-target-iqn-settin
   templateUrl: './iscsi-target-form.component.html',
   styleUrls: ['./iscsi-target-form.component.scss']
 })
-export class IscsiTargetFormComponent {
+export class IscsiTargetFormComponent implements OnInit {
   targetForm: CdFormGroup;
   modalRef: BsModalRef;
   minimum_gateways = 1;
   target_default_controls: any;
   disk_default_controls: any;
 
+  isEdit = false;
+  target_iqn: string;
+
   imagesAll: any[];
   imagesSelections: SelectOption[];
   portalsSelections: SelectOption[] = [];
-  imagesInitiatorSelections: SelectOption[] = [];
-  groupDiskSelections: SelectOption[] = [];
-  groupMembersSelections: SelectOption[] = [];
+
+  imagesInitiatorSelections: SelectOption[][] = [];
+  groupDiskSelections: SelectOption[][] = [];
+  groupMembersSelections: SelectOption[][] = [];
+
   imagesSettings: any = {};
   messages = {
     portals: new SelectMessages(
@@ -73,39 +78,64 @@ export class IscsiTargetFormComponent {
     private modalService: BsModalService,
     private rbdService: RbdService,
     private router: Router,
+    private route: ActivatedRoute,
     private i18n: I18n,
     private taskWrapper: TaskWrapperService
-  ) {
-    forkJoin([this.rbdService.list(), this.iscsiService.listTargets()]).subscribe((data: any[]) => {
-      const usedImages = _(data[1])
+  ) {}
+
+  ngOnInit() {
+    const promises: any[] = [
+      this.iscsiService.listTargets(),
+      this.rbdService.list(),
+      this.iscsiService.portals(),
+      this.iscsiService.settings()
+    ];
+
+    if (this.router.url.startsWith('/block/iscsi/targets/edit')) {
+      this.isEdit = true;
+      this.route.params.subscribe((params: { target_iqn: string }) => {
+        this.target_iqn = decodeURIComponent(params.target_iqn);
+        promises.push(this.iscsiService.getTarget(this.target_iqn));
+      });
+    }
+
+    forkJoin(promises).subscribe((data: any[]) => {
+      // iscsiService.listTargets
+      const usedImages = _(data[0])
+        .filter((target) => target.target_iqn !== this.target_iqn)
         .flatMap((target) => target.disks)
         .map((image) => `${image.pool}/${image.image}`)
         .value();
 
-      this.imagesAll = _(data[0])
+      // rbdService.list()
+      this.imagesAll = _(data[1])
         .flatMap((pool) => pool.value)
         .map((image) => `${image.pool_name}/${image.name}`)
         .filter((image) => usedImages.indexOf(image) === -1)
         .value();
 
       this.imagesSelections = this.imagesAll.map((image) => new SelectOption(false, image, ''));
-    });
 
-    this.iscsiService.portals().subscribe((result: any) => {
+      // iscsiService.portals()
       const portals: SelectOption[] = [];
-      result.forEach((portal) => {
+      data[2].forEach((portal) => {
         portal.ip_addresses.forEach((ip) => {
           portals.push(new SelectOption(false, portal.name + ':' + ip, ''));
         });
       });
       this.portalsSelections = [...portals];
-    });
 
-    this.iscsiService.settings().subscribe((result: any) => {
-      this.minimum_gateways = result.config.minimum_gateways;
-      this.target_default_controls = result.target_default_controls;
-      this.disk_default_controls = result.disk_default_controls;
+      // iscsiService.settings()
+      this.minimum_gateways = data[3].config.minimum_gateways;
+      this.target_default_controls = data[3].target_default_controls;
+      this.disk_default_controls = data[3].disk_default_controls;
+
       this.createForm();
+
+      // iscsiService.getTarget()
+      if (data[4]) {
+        this.resolveModel(data[4]);
+      }
     });
   }
 
@@ -129,6 +159,50 @@ export class IscsiTargetFormComponent {
     });
   }
 
+  resolveModel(res) {
+    this.targetForm.patchValue({
+      target_iqn: res.target_iqn,
+      target_controls: res.target_controls
+    });
+
+    const portals = [];
+    _.forEach(res.portals, (portal) => {
+      const id = `${portal.host}:${portal.ip}`;
+      portals.push(id);
+    });
+    this.targetForm.patchValue({
+      portals: portals
+    });
+
+    const disks = [];
+    _.forEach(res.disks, (disk) => {
+      const id = `${disk.pool}/${disk.image}`;
+      disks.push(id);
+      this.imagesSettings[id] = disk.controls;
+      this.onImageSelection({ option: { name: id, selected: true } });
+    });
+    this.targetForm.patchValue({
+      disks: disks
+    });
+
+    _.forEach(res.clients, (client) => {
+      const initiator = this.addInitiator();
+      client.luns = _.map(client.luns, (lun) => `${lun.pool}/${lun.image}`);
+      initiator.patchValue(client);
+      // updatedInitiatorSelector()
+    });
+
+    _.forEach(res.groups, (group) => {
+      const fg = this.addGroup();
+      console.log(group);
+      group.disks = _.map(group.disks, (disk) => `${disk.pool}/${disk.image}`);
+      fg.patchValue(group);
+      _.forEach(group.members, (member) => {
+        this.onGroupMemberSelection({ option: new SelectOption(true, member, '') });
+      });
+    });
+  }
+
   hasAdvancedSettings(settings: any) {
     return Object.values(settings).length > 0;
   }
@@ -138,7 +212,7 @@ export class IscsiTargetFormComponent {
     return this.targetForm.get('portals') as FormControl;
   }
 
-  onPortalSelection($event) {
+  onPortalSelection() {
     this.portals.setValue(this.portals.value);
   }
 
@@ -181,10 +255,12 @@ export class IscsiTargetFormComponent {
       element.get('disks').setValue(newDisks);
     });
 
-    this.imagesInitiatorSelections = this.imagesInitiatorSelections.filter(
-      (item) => item.name !== name
-    );
-    this.groupDiskSelections = this.groupDiskSelections.filter((item) => item.name !== name);
+    _.forEach(this.imagesInitiatorSelections, (selections, i) => {
+      this.imagesInitiatorSelections[i] = selections.filter((item: any) => item.name !== name);
+    });
+    _.forEach(this.groupDiskSelections, (selections, i) => {
+      this.groupDiskSelections[i] = selections.filter((item: any) => item.name !== name);
+    });
   }
 
   onImageSelection($event) {
@@ -194,14 +270,19 @@ export class IscsiTargetFormComponent {
       if (!this.imagesSettings[option.name]) {
         this.imagesSettings[option.name] = {};
       }
-      this.imagesInitiatorSelections.push(new SelectOption(false, option.name, ''));
-      this.groupDiskSelections.push(new SelectOption(false, option.name, ''));
+
+      _.forEach(this.imagesInitiatorSelections, (selections, i) => {
+        selections.push(new SelectOption(false, option.name, ''));
+        this.imagesInitiatorSelections[i] = [...selections];
+      });
+
+      _.forEach(this.groupDiskSelections, (selections, i) => {
+        selections.push(new SelectOption(false, option.name, ''));
+        this.groupDiskSelections[i] = [...selections];
+      });
     } else {
       this.removeImageRefs(option.name);
     }
-
-    this.imagesInitiatorSelections = [...this.imagesInitiatorSelections];
-    this.groupDiskSelections = [...this.groupDiskSelections];
   }
 
   // Initiators
@@ -268,22 +349,36 @@ export class IscsiTargetFormComponent {
 
     this.initiators.push(fg);
 
-    this.groupMembersSelections.push(new SelectOption(false, '', ''));
-    this.groupMembersSelections = [...this.groupMembersSelections];
+    _.forEach(this.groupMembersSelections, (selections, i) => {
+      selections.push(new SelectOption(false, '', ''));
+      this.groupMembersSelections[i] = [...selections];
+    });
 
-    return false;
+    const disks = _.map(
+      this.targetForm.getValue('disks'),
+      (disk) => new SelectOption(false, disk, '')
+    );
+    this.imagesInitiatorSelections.push(disks);
+
+    return fg;
   }
 
   removeInitiator(index) {
+    const removed = this.initiators.value[index];
+
     this.initiators.removeAt(index);
 
-    const removed: SelectOption[] = this.groupMembersSelections.splice(index, 1);
-    this.groupMembersSelections = [...this.groupMembersSelections];
+    _.forEach(this.groupMembersSelections, (selections, i) => {
+      selections.splice(index, 1);
+      this.groupMembersSelections[i] = [...selections];
+    });
 
     this.groups.controls.forEach((element) => {
-      const newMembers = element.value.members.filter((item) => item !== removed[0].name);
+      const newMembers = element.value.members.filter((item) => item !== removed.client_iqn);
       element.get('members').setValue(newMembers);
     });
+
+    this.imagesInitiatorSelections.splice(index, 1);
   }
 
   updatedInitiatorSelector() {
@@ -293,21 +388,37 @@ export class IscsiTargetFormComponent {
     });
 
     // Update Group Initiator Selector
-    this.groupMembersSelections.forEach((elem, index) => {
-      const oldName = elem.name;
-      elem.name = this.initiators.controls[index].value.client_iqn;
-
-      this.groups.controls.forEach((element) => {
-        const members = element.value.members;
-        const i = members.indexOf(oldName);
-
-        if (i !== -1) {
-          members[i] = elem.name;
-        }
-        element.get('members').setValue(members);
+    _.forEach(this.groupMembersSelections, (group, group_index) => {
+      _.forEach(group, (elem, index) => {
+        const oldName = elem.name;
+        elem.name = this.initiators.controls[index].value.client_iqn;
+
+        this.groups.controls.forEach((element) => {
+          const members = element.value.members;
+          const i = members.indexOf(oldName);
+
+          if (i !== -1) {
+            members[i] = elem.name;
+          }
+          element.get('members').setValue(members);
+        });
       });
+      this.groupMembersSelections[group_index] = [...this.groupMembersSelections[group_index]];
     });
-    this.groupMembersSelections = [...this.groupMembersSelections];
+  }
+
+  removeInitiatorImage(initiator: any, lun_index: number, initiator_index: string, image: string) {
+    const luns = initiator.getValue('luns');
+    luns.splice(lun_index, 1);
+    initiator.patchValue({ luns: luns });
+
+    this.imagesInitiatorSelections[initiator_index].forEach((value) => {
+      if (value.name === image) {
+        value.selected = false;
+      }
+    });
+
+    return false;
   }
 
   // Groups
@@ -316,14 +427,32 @@ export class IscsiTargetFormComponent {
   }
 
   addGroup() {
-    this.groups.push(
-      new CdFormGroup({
-        group_id: new FormControl('', { validators: [Validators.required] }),
-        members: new FormControl([]),
-        disks: new FormControl([])
-      })
+    const fg = new CdFormGroup({
+      group_id: new FormControl('', { validators: [Validators.required] }),
+      members: new FormControl([]),
+      disks: new FormControl([])
+    });
+
+    this.groups.push(fg);
+
+    const disks = _.map(
+      this.targetForm.getValue('disks'),
+      (disk) => new SelectOption(false, disk, '')
     );
-    return false;
+    this.groupDiskSelections.push(disks);
+
+    const initiators = _.map(
+      this.initiators.value,
+      (initiator) => new SelectOption(false, initiator.client_iqn, '')
+    );
+    this.groupMembersSelections.push(initiators);
+
+    return fg;
+  }
+
+  removeGroup(index) {
+    this.groups.removeAt(index);
+    this.groupDiskSelections.splice(index, 1);
   }
 
   onGroupMemberSelection($event) {
@@ -336,20 +465,33 @@ export class IscsiTargetFormComponent {
       }
     });
   }
-  removeGroupInitiator(group, i) {
-    const name = group.getValue('members')[i];
-    group.getValue('members').splice(i, 1);
 
-    this.groupMembersSelections.forEach((value) => {
+  removeGroupInitiator(group, member_index, group_index) {
+    const name = group.getValue('members')[member_index];
+    group.getValue('members').splice(member_index, 1);
+
+    this.groupMembersSelections[group_index].forEach((value) => {
       if (value.name === name) {
         value.selected = false;
       }
     });
-    this.groupMembersSelections = [...this.groupMembersSelections];
+    this.groupMembersSelections[group_index] = [...this.groupMembersSelections[group_index]];
 
     this.onGroupMemberSelection({ option: new SelectOption(false, name, '') });
   }
 
+  removeGroupDisk(group, disk_index, group_index) {
+    const name = group.getValue('disks')[disk_index];
+    group.getValue('disks').splice(disk_index, 1);
+
+    this.groupDiskSelections[group_index].forEach((value) => {
+      if (value.name === name) {
+        value.selected = false;
+      }
+    });
+    this.groupDiskSelections[group_index] = [...this.groupDiskSelections[group_index]];
+  }
+
   submit() {
     const formValue = this.targetForm.value;
 
@@ -424,20 +566,32 @@ export class IscsiTargetFormComponent {
     });
     request.groups = formValue.groups;
 
-    this.taskWrapper
-      .wrapTaskAroundCall({
+    let wrapTask;
+    if (this.isEdit) {
+      request['new_target_iqn'] = request.target_iqn;
+      request.target_iqn = this.target_iqn;
+      wrapTask = this.taskWrapper.wrapTaskAroundCall({
+        task: new FinishedTask('iscsi/target/edit', {
+          target_iqn: request.target_iqn
+        }),
+        call: this.iscsiService.updateTarget(this.target_iqn, request)
+      });
+    } else {
+      wrapTask = this.taskWrapper.wrapTaskAroundCall({
         task: new FinishedTask('iscsi/target/create', {
           target_iqn: request.target_iqn
         }),
         call: this.iscsiService.createTarget(request)
-      })
-      .subscribe(
-        undefined,
-        () => {
-          this.targetForm.setErrors({ cdSubmitButton: true });
-        },
-        () => this.router.navigate(['/block/iscsi/targets'])
-      );
+      });
+    }
+
+    wrapTask.subscribe(
+      undefined,
+      () => {
+        this.targetForm.setErrors({ cdSubmitButton: true });
+      },
+      () => this.router.navigate(['/block/iscsi/targets'])
+    );
   }
 
   targetSettingsModal() {
index 299ef33deaa84d119e2f84f9a46cec10e8b7657c..7a48d2ab6bf22fc24ddab198c0df1332ce8fc957 100644 (file)
@@ -189,7 +189,7 @@ describe('IscsiTargetListComponent', () => {
       );
       scenario = {
         fn: () => tableActions.getCurrentButton().name,
-        single: 'Delete',
+        single: 'Edit',
         empty: 'Add'
       };
     });
@@ -199,11 +199,12 @@ describe('IscsiTargetListComponent', () => {
         tableActions = permissionHelper.setPermissionsAndGetActions(1, 1, 1);
       });
 
-      it(`shows 'Delete' for single selection else 'Add' as main action`, () =>
-        permissionHelper.testScenarios(scenario));
+      it(`shows 'Edit' for single selection else 'Add' as main action`, () => {
+        permissionHelper.testScenarios(scenario);
+      });
 
       it('shows all actions', () => {
-        expect(tableActions.tableActions.length).toBe(2);
+        expect(tableActions.tableActions.length).toBe(3);
         expect(tableActions.tableActions).toEqual(component.tableActions);
       });
     });
@@ -211,15 +212,15 @@ describe('IscsiTargetListComponent', () => {
     describe('with read, create and update', () => {
       beforeEach(() => {
         tableActions = permissionHelper.setPermissionsAndGetActions(1, 1, 0);
-        scenario.single = 'Add';
+        scenario.single = 'Edit';
       });
 
-      it(`should always show 'Add'`, () => {
+      it(`should always show 'Edit'`, () => {
         permissionHelper.testScenarios(scenario);
       });
 
       it(`shows all actions except for 'Delete'`, () => {
-        expect(tableActions.tableActions.length).toBe(1);
+        expect(tableActions.tableActions.length).toBe(2);
         component.tableActions.pop();
         expect(tableActions.tableActions).toEqual(component.tableActions);
       });
@@ -239,7 +240,7 @@ describe('IscsiTargetListComponent', () => {
         expect(tableActions.tableActions.length).toBe(2);
         expect(tableActions.tableActions).toEqual([
           component.tableActions[0],
-          component.tableActions[1]
+          component.tableActions[2]
         ]);
       });
     });
@@ -249,14 +250,17 @@ describe('IscsiTargetListComponent', () => {
         tableActions = permissionHelper.setPermissionsAndGetActions(0, 1, 1);
       });
 
-      it(`shows always 'Delete' as main action`, () => {
-        scenario.empty = 'Delete';
+      it(`shows always 'Edit' as main action`, () => {
+        scenario.empty = 'Edit';
         permissionHelper.testScenarios(scenario);
       });
 
-      it(`shows 'Delete' action`, () => {
-        expect(tableActions.tableActions.length).toBe(1);
-        expect(tableActions.tableActions).toEqual([component.tableActions[1]]);
+      it(`shows 'Edit' and 'Delete' actions`, () => {
+        expect(tableActions.tableActions.length).toBe(2);
+        expect(tableActions.tableActions).toEqual([
+          component.tableActions[1],
+          component.tableActions[2]
+        ]);
       });
     });
 
@@ -282,8 +286,8 @@ describe('IscsiTargetListComponent', () => {
       });
 
       it(`shows no actions`, () => {
-        expect(tableActions.tableActions.length).toBe(0);
-        expect(tableActions.tableActions).toEqual([]);
+        expect(tableActions.tableActions.length).toBe(1);
+        expect(tableActions.tableActions).toEqual([component.tableActions[1]]);
       });
     });
 
@@ -300,7 +304,7 @@ describe('IscsiTargetListComponent', () => {
 
       it(`shows 'Delete' actions`, () => {
         expect(tableActions.tableActions.length).toBe(1);
-        expect(tableActions.tableActions).toEqual([component.tableActions[1]]);
+        expect(tableActions.tableActions).toEqual([component.tableActions[2]]);
       });
     });
 
index f5150d12b90f169164b1a16f7d5b616db91e2b83..a798e58b274fe5de3a213025d0c85f5c715910a1 100644 (file)
@@ -69,6 +69,12 @@ export class IscsiTargetListComponent implements OnInit, OnDestroy {
         routerLink: () => '/block/iscsi/targets/add',
         name: this.i18n('Add')
       },
+      {
+        permission: 'update',
+        icon: 'fa-pencil',
+        routerLink: () => `/block/iscsi/targets/edit/${this.selection.first().target_iqn}`,
+        name: this.i18n('Edit')
+      },
       {
         permission: 'delete',
         icon: 'fa-times',
@@ -148,7 +154,7 @@ export class IscsiTargetListComponent implements OnInit, OnDestroy {
   }
 
   taskFilter(task) {
-    return ['iscsi/target/create', 'iscsi/target/delete'].includes(task.name);
+    return ['iscsi/target/create', 'iscsi/target/edit', 'iscsi/target/delete'].includes(task.name);
   }
 
   updateSelection(selection: CdTableSelection) {
index fee52375eda7feb5b64d4b52413b180aacab68be..7eb21db917344583ad05d4eada34cb181b590efb 100644 (file)
@@ -32,6 +32,12 @@ describe('IscsiService', () => {
     expect(req.request.method).toBe('GET');
   });
 
+  it('should call getTarget', () => {
+    service.getTarget('iqn.foo').subscribe();
+    const req = httpTesting.expectOne('api/iscsi/target/iqn.foo');
+    expect(req.request.method).toBe('GET');
+  });
+
   it('should call status', () => {
     service.status().subscribe();
     const req = httpTesting.expectOne('ui-api/iscsi/status');
@@ -51,10 +57,17 @@ describe('IscsiService', () => {
   });
 
   it('should call createTarget', () => {
-    service.createTarget('foo').subscribe();
+    service.createTarget({ target_iqn: 'foo' }).subscribe();
     const req = httpTesting.expectOne('api/iscsi/target');
     expect(req.request.method).toBe('POST');
-    expect(req.request.body).toEqual('foo');
+    expect(req.request.body).toEqual({ target_iqn: 'foo' });
+  });
+
+  it('should call updateTarget', () => {
+    service.updateTarget('iqn.foo', { target_iqn: 'foo' }).subscribe();
+    const req = httpTesting.expectOne('api/iscsi/target/iqn.foo');
+    expect(req.request.method).toBe('PUT');
+    expect(req.request.body).toEqual({ target_iqn: 'foo' });
   });
 
   it('should call deleteTarget', () => {
index 014987283078809e2f0ee2627cb75c720e540bcb..b829ea95dc644dde4ef70824021c0cbe4b87a85e 100644 (file)
@@ -1,8 +1,10 @@
 import { HttpClient } from '@angular/common/http';
 import { Injectable } from '@angular/core';
 
+import { cdEncode } from '../decorators/cd-encode';
 import { ApiModule } from './api.module';
 
+@cdEncode
 @Injectable({
   providedIn: ApiModule
 })
@@ -64,6 +66,14 @@ export class IscsiService {
     return this.http.get(`api/iscsi/target`);
   }
 
+  getTarget(target_iqn) {
+    return this.http.get(`api/iscsi/target/${target_iqn}`);
+  }
+
+  updateTarget(target_iqn, target) {
+    return this.http.put(`api/iscsi/target/${target_iqn}`, target, { observe: 'response' });
+  }
+
   status() {
     return this.http.get(`ui-api/iscsi/status`);
   }
@@ -80,8 +90,8 @@ export class IscsiService {
     return this.http.post(`api/iscsi/target`, target, { observe: 'response' });
   }
 
-  deleteTarget(targetIqn) {
-    return this.http.delete(`api/iscsi/target/${targetIqn}`, { observe: 'response' });
+  deleteTarget(target_iqn) {
+    return this.http.delete(`api/iscsi/target/${target_iqn}`, { observe: 'response' });
   }
 
   getDiscovery() {
index 3c79928e592f304d17a689fe54022a29436e9d9b..b02c49a0868daba0080233334f13f94bc005c180 100644 (file)
           <context context-type="sourcefile">app/ceph/block/iscsi-target-iqn-settings-modal/iscsi-target-iqn-settings-modal.component.html</context>
           <context context-type="linenumber">3</context>
         </context-group>
-      </trans-unit><trans-unit id="a01e6937a5d1ee040a02416eed34544c4ea61e38" datatype="html">
-        <source>Create target</source>
+      </trans-unit><trans-unit id="602ace8a29cf86e2e7db2f6b0cdb896ddf760e0d" datatype="html">
+        <source><x id="ICU" equiv-text="{isEdit, select, 1 {...} other {...}}"/> target</source>
         <context-group purpose="location">
           <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
           <context context-type="linenumber">11</context>
         </context-group>
+      </trans-unit><trans-unit id="457db3d876e89cae583be9845912213ae73dc398" datatype="html">
+        <source>{VAR_SELECT, select, 1 {Edit} other {Add} }</source>
         <context-group purpose="location">
           <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
-          <context context-type="linenumber">539</context>
+          <context context-type="linenumber">11</context>
         </context-group>
       </trans-unit><trans-unit id="1406c2fb12a20c1528b19bcc5e24a6a2386167f3" datatype="html">
         <source>Target IQN</source>
           <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
           <context context-type="linenumber">526</context>
         </context-group>
+      </trans-unit><trans-unit id="71c77bb8cecdf11ec3eead24dd1ba506573fa9cd" datatype="html">
+        <source>Submit</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/block/iscsi-target-form/iscsi-target-form.component.html</context>
+          <context context-type="linenumber">539</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/block/iscsi-target-discovery-modal/iscsi-target-discovery-modal.component.html</context>
+          <context context-type="linenumber">133</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.html</context>
+          <context context-type="linenumber">34</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/cluster/osd/osd-recv-speed-modal/osd-recv-speed-modal.component.html</context>
+          <context context-type="linenumber">87</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/cluster/osd/osd-scrub-modal/osd-scrub-modal.component.html</context>
+          <context context-type="linenumber">21</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/block/mirroring/pool-edit-peer-modal/pool-edit-peer-modal.component.html</context>
+          <context context-type="linenumber">106</context>
+        </context-group>
       </trans-unit><trans-unit id="54839ebc827b73c9dc4e1df731c6d36a85036af7" datatype="html">
         <source>Are you sure that you want to <x id="INTERPOLATION" equiv-text="{{ actionDescription | lowercase }}"/> the selected <x id="INTERPOLATION_1" equiv-text="{{ itemDescription }}"/>?</source>
         <context-group purpose="location">
           <context context-type="sourcefile">app/ceph/block/iscsi-target-discovery-modal/iscsi-target-discovery-modal.component.html</context>
           <context context-type="linenumber">123</context>
         </context-group>
-      </trans-unit><trans-unit id="71c77bb8cecdf11ec3eead24dd1ba506573fa9cd" datatype="html">
-        <source>Submit</source>
-        <context-group purpose="location">
-          <context context-type="sourcefile">app/ceph/block/iscsi-target-discovery-modal/iscsi-target-discovery-modal.component.html</context>
-          <context context-type="linenumber">133</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">app/ceph/cluster/osd/osd-flags-modal/osd-flags-modal.component.html</context>
-          <context context-type="linenumber">34</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">app/ceph/cluster/osd/osd-recv-speed-modal/osd-recv-speed-modal.component.html</context>
-          <context context-type="linenumber">87</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">app/ceph/cluster/osd/osd-scrub-modal/osd-scrub-modal.component.html</context>
-          <context context-type="linenumber">21</context>
-        </context-group>
-        <context-group purpose="location">
-          <context context-type="sourcefile">app/ceph/block/mirroring/pool-edit-peer-modal/pool-edit-peer-modal.component.html</context>
-          <context context-type="linenumber">106</context>
-        </context-group>
       </trans-unit><trans-unit id="53a583cd5f15059cc958b7d547f72cc78f68e123" datatype="html">
         <source>Please consult the <x id="START_LINK" ctype="x-a" equiv-text="&lt;a&gt;"/>documentation<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a&gt;"/>
     on how to configure and enable the iSCSI Targets management functionality.</source>