]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: create smb cluster 60819/head
authorDnyaneshwari <dnyaneshwari@li-9c9fbecc-2d5c-11b2-a85c-e2a7cc8a424f.ibm.com>
Tue, 19 Nov 2024 07:01:52 +0000 (12:31 +0530)
committerDnyaneshwari <dnyaneshwari@li-9c9fbecc-2d5c-11b2-a85c-e2a7cc8a424f.ibm.com>
Tue, 28 Jan 2025 08:58:56 +0000 (14:28 +0530)
Fixes: https://tracker.ceph.com/issues/69156
Signed-off-by: Dnyaneshwari Talwekar <dtalwekar@redhat.com>
19 files changed:
src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-move-modal/rbd-trash-move-modal.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-form/cephfs-snapshotschedule-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-list/smb-cluster-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-list/smb-cluster-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-domain-setting-modal/smb-domain-setting-modal.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-domain-setting-modal/smb-domain-setting-modal.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-domain-setting-modal/smb-domain-setting-modal.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-domain-setting-modal/smb-domain-setting-modal.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.model.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/service.interface.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts

index 89b2de5513ff3171fb16b51cc1e63171df58f3cb..f7a139e5b9d9fab6f5ed20e485f8c915c32c65f9 100644 (file)
@@ -52,6 +52,7 @@ import { MultiClusterComponent } from './ceph/cluster/multi-cluster/multi-cluste
 import { MultiClusterListComponent } from './ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component';
 import { MultiClusterDetailsComponent } from './ceph/cluster/multi-cluster/multi-cluster-details/multi-cluster-details.component';
 import { SmbClusterListComponent } from './ceph/smb/smb-cluster-list/smb-cluster-list.component';
+import { SmbClusterFormComponent } from './ceph/smb/smb-cluster-form/smb-cluster-form.component';
 
 @Injectable()
 export class PerformanceCounterBreadcrumbsResolver extends BreadcrumbsResolver {
@@ -444,7 +445,14 @@ const routes: Routes = [
               },
               breadcrumbs: 'File/SMB'
             },
-            children: [{ path: '', component: SmbClusterListComponent }]
+            children: [
+              { path: '', component: SmbClusterListComponent },
+              {
+                path: `${URLVerbs.CREATE}`,
+                component: SmbClusterFormComponent,
+                data: { breadcrumbs: ActionLabels.CREATE }
+              }
+            ]
           }
         ]
       },
index efee2aacfba1db26de7b788b2158c5d5aa549b6a..3816601f781ab42a746cfd2986ae2441ddadcf07 100644 (file)
@@ -104,7 +104,7 @@ describe('RbdTrashMoveModalComponent', () => {
       component.moveImage();
       const req = httpTesting.expectOne('api/block/image/foo%2Fbar/move_trash');
       req.flush(null);
-      expect(req.request.body.delay).toBeGreaterThan(76390);
+      expect(req.request.body.delay).toBeGreaterThan(56666);
     });
   });
 });
index 1a611dc18d789b6cebc7a33711fde68954d7c297..f1a2dcb6b4b746c726aa61fec7f349f0c3996268 100644 (file)
               <div cdsCol
                    [columnNumbers]="{lg: 1}"
                    class="item-action-btn">
-                <cds-icon-button kind="tertiary"
+                <cds-icon-button kind="danger"
                                  size="sm"
                                  (click)="removeRetentionPolicy(i)">
                   <svg cdsIcon="trash-can"
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.html
new file mode 100644 (file)
index 0000000..887727c
--- /dev/null
@@ -0,0 +1,330 @@
+<div cdsCol
+     [columnNumbers]="{ md: 4 }"
+     *ngIf="orchStatus$ | async as orchStatus">
+  <form name="smbForm"
+        #formDir="ngForm"
+        [formGroup]="smbForm"
+        novalidate>
+    <div i18n="form title"
+         class="form-header">
+      {{ action | titlecase }} {{ resource | upperFirst }}
+    </div>
+
+    <!-- Cluster Id -->
+    <div class="form-item">
+      <cds-text-label
+        labelInputID="cluster_id"
+        i18n
+        helperText="Unique cluster identifier"
+        i18n-helperText
+        cdRequiredField="Cluster Name"
+        [invalid]="smbForm.controls.cluster_id.invalid && smbForm.controls.cluster_id.dirty"
+        [invalidText]="clusterError"
+        >Cluster Name
+        <input
+          cdsText
+          type="text"
+          placeholder="Cluster Name..."
+          i18n-placeholder
+          id="cluster_id"
+          formControlName="cluster_id"
+          [invalid]="smbForm.controls.cluster_id.invalid && smbForm.controls.cluster_id.dirty"
+        />
+      </cds-text-label>
+      <ng-template #clusterError>
+        <span
+          class="invalid-feedback"
+          *ngIf="smbForm.showError('cluster_id', formDir, 'required')"
+          i18n
+          >This field is required.</span
+        >
+      </ng-template>
+    </div>
+
+    <!-- Auth Mode -->
+    <div class="form-item">
+      <cds-select
+        formControlName="auth_mode"
+        label="Authentication Mode"
+        cdRequiredField="Authentication Mode"
+        id="auth_mode"
+        [invalid]="smbForm.controls.auth_mode.invalid && smbForm.controls.auth_mode.dirty"
+        [invalidText]="authModeError"
+        (change)="onAuthModeChange()"
+        helperText="Active-directory authentication for domain member servers and User authentication for
+        Stand-alone servers configuration."
+        i18n-helperText
+      >
+        <option value="active-directory"
+                i18n>Active Directory</option>
+        <option value="user"
+                i18n>User</option>
+      </cds-select>
+      <ng-template #authModeError>
+        <span
+          class="invalid-feedback"
+          *ngIf="smbForm.showError('auth_mode', formDir, 'required')"
+          i18n
+          >This field is required.</span
+        >
+      </ng-template>
+    </div>
+
+    <!-- Domain Settings -->
+    <div class="form-item"
+         *ngIf="this.smbForm.get('auth_mode').value === 'active-directory'">
+      <div cdsCol
+           [columnNumbers]="{ md: 12 }"
+           class="d-flex">
+        <cds-text-label labelInputID="domain_settings"
+                        i18n
+                        cdRequiredField="Domain Settings">Domain Settings
+          <div class="cds-input-group">
+            <input
+              cdsText
+              type="text"
+              placeholder="Domain Settings..."
+              i18n-placeholder
+              id="domain_settings"
+              formControlName="domain_settings"
+              [value]="domainSettingsObject?.realm"
+              (click)="editDomainSettingsModal()"
+              [invalid]="
+                !smbForm.controls.domain_settings.valid &&
+                smbForm.controls.domain_settings.dirty &&
+                smbForm.controls.domain_settings.touched
+              "
+            />
+            <cds-icon-button kind="ghost"
+                             (click)="editDomainSettingsModal()"
+                             size="md">
+              <svg cdsIcon="edit"
+                   size="32"
+                   class="cds--btn__icon"
+                   icon></svg>
+            </cds-icon-button>
+
+            <cds-icon-button kind="danger"
+                             (click)="deleteDomainSettingsModal()"
+                             size="md">
+              <svg cdsIcon="trash-can"
+                   size="32"
+                   class="cds--btn__icon"
+                   icon></svg>
+            </cds-icon-button>
+          </div>
+        </cds-text-label>
+      </div>
+      <span
+        class="invalid-feedback"
+        *ngIf="
+          smbForm.get('domain_settings').hasError('required') &&
+          smbForm.controls.domain_settings.touched
+        "
+        i18n>Specify the Realm and Join Sources in the Domain Settings field.</span
+      >
+      <div></div>
+    </div>
+
+    <!-- User Group Settings -->
+    <ng-container formArrayName="joinSources"
+                  *ngFor="let dns of joinSources.controls; index as i">
+      <div
+        cdsRow
+        *ngIf="this.smbForm.get('auth_mode').value === 'user'"
+        class="form-item form-item-append"
+      >
+        <div cdsCol
+             [columnNumbers]="{ lg: 14 }">
+          <cds-text-label for="joinSources"
+                          i18n
+                          cdRequiredField="User Group Id">User Group Id
+            <input
+              cdsText
+              type="text"
+              placeholder="User Group Id"
+              i18n-placeholder
+              [id]="'joinSources-' + i"
+              [formControlName]="i"
+              [invalid]="
+                smbForm.controls['joinSources'].controls[i].invalid &&
+                smbForm.controls['joinSources'].dirty
+              "
+            />
+          </cds-text-label>
+          <ng-template #refError>
+            <span
+              class="invalid-feedback"
+              *ngIf="smbForm.showError('joinSources[i]', formDir, 'required')"
+              i18n
+              >This field is required.</span
+            >
+          </ng-template>
+        </div>
+        <div cdsCol
+             [columnNumbers]="{ lg: 1 }">
+          <cds-icon-button
+            kind="danger"
+            *ngIf="i > 0"
+            size="sm"
+            (click)="removeUserGroupSetting(i)"
+          >
+            <svg cdsIcon="trash-can"
+                 size="32"
+                 class="cds--btn__icon"></svg>
+          </cds-icon-button>
+        </div>
+      </div>
+    </ng-container>
+
+    <div class="form-item"
+         *ngIf="this.smbForm.get('auth_mode').value === 'user'">
+      <button cdsButton="tertiary"
+              type="button"
+              (click)="addUserGroupSetting()"
+              i18n>
+        Add User Group Id
+        <svg cdsIcon="add"
+             size="32"
+             class="cds--btn__icon"
+             icon></svg>
+      </button>
+    </div>
+
+    <!-- Placement -->
+    <ng-container *ngIf="orchStatus.available">
+      <div class="form-item">
+        <cds-select
+          label="Placement"
+          for="placement"
+          formControlName="placement"
+          id="placement"
+        >
+          <option value="hosts"
+                  i18n>Hosts</option>
+          <option value="label"
+                  i18n>Labels</option>
+        </cds-select>
+      </div>
+      <ng-container *ngIf="hostsAndLabels$ | async as data">
+        <!-- Label -->
+        <div *ngIf="smbForm.controls.placement.value === 'label'"
+             class="form-item">
+          <cds-combo-box
+            type="multi"
+            selectionFeedback="top-after-reopen"
+            label="Label"
+            formControlName="label"
+            id="label"
+            placeholder="Select labels..."
+            [appendInline]="true"
+            [items]="data.labels"
+            i18n-placeholder
+            (selected)="multiSelector($event, 'label')"
+            [invalid]="smbForm.controls.label.invalid && smbForm.controls.label.dirty"
+            [invalidText]="labelError"
+            cdRequiredField="Label"
+            i18n
+          >
+            <cds-dropdown-list></cds-dropdown-list>
+          </cds-combo-box>
+          <ng-template #labelError>
+            <span
+              class="invalid-feedback"
+              *ngIf="smbForm.showError('label', formDir, 'required')"
+              i18n
+              >This field is required.</span
+            >
+          </ng-template>
+        </div>
+
+        <!-- Hosts -->
+        <div *ngIf="smbForm.controls.placement.value === 'hosts'"
+             class="form-item">
+          <cds-combo-box
+            type="multi"
+            selectionFeedback="top-after-reopen"
+            label="Hosts"
+            formControlName="hosts"
+            id="hosts"
+            placeholder="Select hosts..."
+            i18n-placeholder
+            [appendInline]="true"
+            [items]="data.hosts"
+            (selected)="multiSelector($event, 'hosts')"
+            i18n
+          >
+            <cds-dropdown-list></cds-dropdown-list>
+          </cds-combo-box>
+        </div>
+      </ng-container>
+    </ng-container>
+
+    <div class="form-item">
+      <cds-number
+        [id]="'count'"
+        [formControlName]="'count'"
+        [label]="'Count'"
+        [min]="1"
+      ></cds-number>
+    </div>
+
+    <!-- Clustering -->
+    <div class="form-item">
+      <cds-select
+        formControlName="clustering"
+        for="clustering"
+        label="Clustering"
+        id="clustering"
+        helperText="Control if a cluster abstraction actually uses Samba’s clustering mechanism."
+        i18n-helperText
+      >
+        <option *ngFor="let data of allClustering"
+                i18n>{{ data | upperFirst }}</option>
+      </cds-select>
+    </div>
+
+    <!-- Custom DNS -->
+    <ng-container formArrayName="custom_dns"
+                  *ngFor="let dns of custom_dns.controls; index as i">
+      <div cdsRow
+           class="form-item form-item-append">
+        <div cdsCol
+             [columnNumbers]="{ lg: 14 }">
+          <input cdsText
+                 [formControlName]="i"
+                 placeholder="Custom DNS"/>
+        </div>
+        <div cdsCol
+             [columnNumbers]="{ lg: 1 }">
+          <cds-icon-button kind="danger"
+                           size="sm"
+                           (click)="removeCustomDNS(i)">
+            <svg cdsIcon="trash-can"
+                 size="32"
+                 class="cds--btn__icon"></svg>
+          </cds-icon-button>
+        </div>
+      </div>
+    </ng-container>
+
+    <div class="form-item">
+      <button cdsButton="tertiary"
+              type="button"
+              (click)="addCustomDns()"
+              i18n>
+        Add Custom DNS
+        <svg cdsIcon="add"
+             size="32"
+             class="cds--btn__icon"
+             icon></svg>
+      </button>
+    </div>
+    <cd-form-button-panel
+      (submitActionEvent)="submitAction()"
+      [form]="smbForm"
+      [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"
+      wrappingClass="text-right"
+    ></cd-form-button-panel>
+  </form>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.spec.ts
new file mode 100644 (file)
index 0000000..73bc10c
--- /dev/null
@@ -0,0 +1,91 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { SmbClusterFormComponent } from './smb-cluster-form.component';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { SharedModule } from '~/app/shared/shared.module';
+import { RouterTestingModule } from '@angular/router/testing';
+import { FormArray, ReactiveFormsModule, Validators } from '@angular/forms';
+import { ToastrModule } from 'ngx-toastr';
+import { ComboBoxModule, GridModule, InputModule, SelectModule } from 'carbon-components-angular';
+import { AUTHMODE } from '../smb.model';
+
+describe('SmbClusterFormComponent', () => {
+  let component: SmbClusterFormComponent;
+  let fixture: ComponentFixture<SmbClusterFormComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [
+        BrowserAnimationsModule,
+        SharedModule,
+        HttpClientTestingModule,
+        RouterTestingModule,
+        ReactiveFormsModule,
+        ToastrModule.forRoot(),
+        GridModule,
+        InputModule,
+        SelectModule,
+        ComboBoxModule
+      ],
+      declarations: [SmbClusterFormComponent]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(SmbClusterFormComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('should have cluster_id and domain_settings as required fields', () => {
+    fixture.detectChanges();
+
+    const clusterIdControl = component.smbForm.get('cluster_id');
+    const domainSettingsControl = component.smbForm.get('domain_settings');
+
+    const isClusterId = [clusterIdControl.validator].includes(Validators.required);
+    const isDomainSettings = [domainSettingsControl.validator].includes(Validators.required);
+
+    expect(isClusterId).toBe(false);
+    expect(isDomainSettings).toBe(true);
+  });
+
+  it('should add and remove user group settings', () => {
+    const defaultLength = component.joinSources.length;
+    component.addUserGroupSetting();
+    expect(component.joinSources.length).toBe(defaultLength + 1);
+    component.removeUserGroupSetting(0);
+    expect(component.joinSources.length).toBe(defaultLength);
+  });
+
+  it('should add and remove custom dns settings (custom_dns)', () => {
+    const defaultLength = component.custom_dns.length;
+    component.addCustomDns();
+    expect(component.custom_dns.length).toBe(defaultLength + 1);
+    component.removeCustomDNS(0);
+    expect(component.custom_dns.length).toBe(defaultLength);
+  });
+
+  it('should change the form when authmode is changed', () => {
+    const authModeControl = component.smbForm.get('auth_mode');
+    authModeControl?.setValue('user');
+    component.onAuthModeChange();
+    fixture.detectChanges();
+    const joinSourcesControl = component.smbForm.get('joinSources') as FormArray;
+    expect(joinSourcesControl.length).toBe(1);
+  });
+
+  it('should check submit request', () => {
+    component.smbForm.get('auth_mode').setValue(AUTHMODE.activeDirectory);
+    component.smbForm.get('domain_settings').setValue('test-realm');
+    component.smbForm.get('cluster_id').setValue('cluster-id');
+    component.submitAction();
+  });
+
+  it('should delete domain', () => {
+    component.deleteDomainSettingsModal();
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.ts
new file mode 100644 (file)
index 0000000..1acbb8e
--- /dev/null
@@ -0,0 +1,309 @@
+import { Component, OnInit } from '@angular/core';
+import { Router } from '@angular/router';
+import { forkJoin, Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+
+import _ from 'lodash';
+import {
+  AUTHMODE,
+  CLUSTERING,
+  PLACEMENT,
+  RequestModel,
+  RESOURCE_TYPE,
+  RESOURCE,
+  DomainSettings,
+  JoinSource
+} from '../smb.model';
+import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
+import { Icons } from '~/app/shared/enum/icons.enum';
+
+import { FormArray, FormControl, UntypedFormControl, Validators } from '@angular/forms';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+
+import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
+import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
+import { HostService } from '~/app/shared/api/host.service';
+import { SmbService } from '~/app/shared/api/smb.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { SmbDomainSettingModalComponent } from '../smb-domain-setting-modal/smb-domain-setting-modal.component';
+import { CephServicePlacement } from '~/app/shared/models/service.interface';
+
+@Component({
+  selector: 'cd-smb-cluster-form',
+  templateUrl: './smb-cluster-form.component.html',
+  styleUrls: ['./smb-cluster-form.component.scss']
+})
+export class SmbClusterFormComponent extends CdForm implements OnInit {
+  smbForm: CdFormGroup;
+  hostsAndLabels$: Observable<{ hosts: any[]; labels: any[] }>;
+  hasOrchestrator: boolean;
+  orchStatus$: Observable<any>;
+  allClustering: string[] = [];
+  selectedLabels: string[] = [];
+  selectedHosts: string[] = [];
+  action: string;
+  resource: string;
+  icons = Icons;
+  domainSettingsObject: DomainSettings;
+  modalData$ = this.smbService.modalData$;
+
+  constructor(
+    private hostService: HostService,
+    private formBuilder: CdFormBuilder,
+    public smbService: SmbService,
+    public actionLabels: ActionLabelsI18n,
+    private orchService: OrchestratorService,
+    private modalService: ModalCdsService,
+    private taskWrapperService: TaskWrapperService,
+    private router: Router
+  ) {
+    super();
+    this.resource = $localize`Cluster`;
+  }
+  ngOnInit() {
+    this.action = this.actionLabels.CREATE;
+    this.smbService.modalData$.subscribe((data: DomainSettings) => {
+      this.domainSettingsObject = data;
+      this.smbForm.get('domain_settings').setValue(data?.realm);
+    });
+    this.createForm();
+
+    this.hostsAndLabels$ = forkJoin({
+      hosts: this.hostService.getAllHosts(),
+      labels: this.hostService.getLabels()
+    }).pipe(
+      map(({ hosts, labels }) => ({
+        hosts: hosts.map((host: any) => ({ content: host['hostname'] })),
+        labels: labels.map((label: string) => ({ content: label }))
+      }))
+    );
+    this.orchStatus$ = this.orchService.status();
+    this.allClustering = Object.values(CLUSTERING);
+    this.onAuthModeChange();
+  }
+
+  createForm() {
+    this.smbForm = this.formBuilder.group({
+      cluster_id: new FormControl('', {
+        validators: [Validators.required]
+      }),
+      auth_mode: [
+        AUTHMODE.activeDirectory,
+        {
+          validators: [Validators.required]
+        }
+      ],
+      domain_settings: [null],
+      placement: [{}],
+      hosts: [[]],
+      label: [
+        null,
+        [
+          CdValidators.requiredIf({
+            placement: 'label'
+          })
+        ]
+      ],
+      count: [1],
+      custom_dns: new FormArray([]),
+      joinSources: new FormArray([]),
+      clustering: new UntypedFormControl(
+        CLUSTERING.Default.charAt(0).toUpperCase() + CLUSTERING.Default.slice(1)
+      )
+    });
+
+    this.orchService.status().subscribe((status) => {
+      this.hasOrchestrator = status.available;
+      this.smbForm.get('placement').setValue(this.hasOrchestrator ? PLACEMENT.host : '');
+    });
+  }
+
+  multiSelector(event: any, field: 'label' | 'hosts') {
+    if (field === PLACEMENT.host) this.selectedLabels = event.map((label: any) => label.content);
+    else this.selectedHosts = event.map((host: any) => host.content);
+  }
+
+  onAuthModeChange() {
+    const authMode = this.smbForm.get('auth_mode').value;
+    const domainSettingsControl = this.smbForm.get('domain_settings');
+    const userGroupSettingsControl = this.smbForm.get('joinSources') as FormArray;
+
+    // User Group Setting should be optional if authMode is "Active Directory"
+    if (authMode === AUTHMODE.activeDirectory) {
+      if (userGroupSettingsControl) {
+        userGroupSettingsControl.clear();
+      }
+      if (domainSettingsControl) {
+        domainSettingsControl.setValidators(Validators.required);
+        domainSettingsControl.updateValueAndValidity();
+      }
+      if (userGroupSettingsControl) {
+        userGroupSettingsControl.clearValidators();
+        userGroupSettingsControl.updateValueAndValidity();
+      }
+      // Domain Setting should be optional if authMode is "Users"
+    } else if (authMode === AUTHMODE.User) {
+      const control = new FormControl('', Validators.required);
+      userGroupSettingsControl.push(control);
+      domainSettingsControl.setErrors(null);
+      domainSettingsControl.clearValidators();
+      userGroupSettingsControl.setValidators(Validators.required);
+    } else {
+      if (userGroupSettingsControl) {
+        userGroupSettingsControl.clearValidators();
+        userGroupSettingsControl.clear();
+        userGroupSettingsControl.updateValueAndValidity();
+      }
+    }
+  }
+
+  submitAction() {
+    const domainSettingsControl = this.smbForm.get('domain_settings');
+    const authMode = this.smbForm.get('auth_mode').value;
+
+    // Domain Setting should be mandatory if authMode is "Active Directory"
+    if (authMode === AUTHMODE.activeDirectory && !domainSettingsControl.value) {
+      domainSettingsControl.setErrors({ required: true });
+      this.smbForm.markAllAsTouched();
+      return;
+    }
+    const component = this;
+    const requestModel = this.buildRequest();
+    const BASE_URL = 'smb/cluster';
+    const cluster_id = this.smbForm.get('cluster_id').value;
+    const taskUrl = `${BASE_URL}/${URLVerbs.CREATE}`;
+    this.taskWrapperService
+      .wrapTaskAroundCall({
+        task: new FinishedTask(taskUrl, { cluster_id }),
+        call: this.smbService.createCluster(requestModel)
+      })
+      .subscribe({
+        complete: () => {
+          this.router.navigate([`cephfs/smb`]);
+        },
+        error() {
+          component.smbForm.setErrors({ cdSubmitButton: true });
+        }
+      });
+  }
+
+  private buildRequest() {
+    const values = this.smbForm.getRawValue();
+    const rawFormValue = _.cloneDeep(this.smbForm.value);
+    const joinSources: JoinSource[] = (this.domainSettingsObject?.join_sources || [])
+      .filter((source: { ref: string }) => source.ref)
+      .map((source: { ref: string }) => ({
+        ref: source.ref,
+        source_type: RESOURCE.Resource
+      }));
+
+    const joinSourceObj = joinSources.map((source: JoinSource) => ({
+      source_type: RESOURCE.Resource,
+      ref: source.ref
+    }));
+
+    const domainSettings = {
+      realm: this.domainSettingsObject?.realm,
+      join_sources: joinSourceObj
+    };
+
+    const requestModel: RequestModel = {
+      cluster_resource: {
+        resource_type: RESOURCE_TYPE,
+        cluster_id: rawFormValue.cluster_id,
+        auth_mode: rawFormValue.auth_mode
+      }
+    };
+
+    if (domainSettings && domainSettings.join_sources.length > 0) {
+      requestModel.cluster_resource.domain_settings = domainSettings;
+    }
+    if (rawFormValue.joinSources?.length > 0) {
+      requestModel.cluster_resource.user_group_settings = rawFormValue.joinSources.map(
+        (source: { ref: string }) => ({
+          source_type: RESOURCE.Resource,
+          ref: source
+        })
+      );
+    }
+
+    const serviceSpec = this.getPlacementSpec(values);
+    if (serviceSpec) {
+      requestModel.cluster_resource.placement = serviceSpec;
+    }
+
+    if (rawFormValue.custom_dns?.length > 0) {
+      requestModel.cluster_resource.custom_dns = rawFormValue.custom_dns;
+    }
+
+    if (rawFormValue.clustering && rawFormValue.clustering.toLowerCase() !== CLUSTERING.Default) {
+      requestModel.cluster_resource.clustering = rawFormValue.clustering.toLowerCase();
+    }
+
+    return requestModel;
+  }
+
+  getPlacementSpec(values: CephServicePlacement) {
+    const serviceSpec = {
+      placement: {}
+    };
+
+    serviceSpec['placement']['count'] = values.count;
+
+    switch (values['placement']) {
+      case 'hosts':
+        if (values['hosts'].length > 0) {
+          serviceSpec['placement']['hosts'] = this.selectedHosts;
+          serviceSpec['placement']['count'] = values.count;
+        }
+        break;
+      case 'label':
+        serviceSpec['placement']['label'] = this.selectedLabels;
+        serviceSpec['placement']['count'] = values.count;
+        break;
+    }
+
+    return serviceSpec.placement;
+  }
+
+  editDomainSettingsModal() {
+    this.modalService.show(SmbDomainSettingModalComponent, {
+      domainSettingsObject: this.domainSettingsObject
+    });
+  }
+
+  deleteDomainSettingsModal() {
+    this.smbForm.get('domain_settings')?.setValue('');
+    this.domainSettingsObject = { realm: '', join_sources: [] };
+  }
+
+  get joinSources() {
+    return this.smbForm.get('joinSources') as FormArray;
+  }
+
+  get custom_dns() {
+    return this.smbForm.get('custom_dns') as FormArray;
+  }
+
+  addUserGroupSetting() {
+    const control = new FormControl('', Validators.required);
+    this.joinSources.push(control);
+  }
+
+  addCustomDns() {
+    const control = new FormControl('', Validators.required);
+    this.custom_dns.push(control);
+  }
+
+  removeUserGroupSetting(index: number) {
+    this.joinSources.removeAt(index);
+  }
+
+  removeCustomDNS(index: number) {
+    this.custom_dns.removeAt(index);
+  }
+}
index cfd7b65dbf47d13db6993312e7bcbf7aa388676f..8d3fa098ad551d1e637b9c0db2aeaffe32d04384 100644 (file)
     (fetchData)="loadSMBCluster($event)"
     (updateSelection)="updateSelection($event)"
   >
-    <div class="table-actions">
-      <cd-table-actions
-        class="btn-group"
-        [permission]="permission"
-        [selection]="selection"
-        [tableActions]="tableActions"
-      >
-      </cd-table-actions>
-    </div>
-  </cd-table>
+  <div class="table-actions">
+    <cd-table-actions class="btn-group"
+                      [permission]="permission"
+                      [selection]="selection"
+                      [tableActions]="tableActions">
+    </cd-table-actions>
+  </div>
+</cd-table>
 </ng-container>
index 018e685d11188ea96316c4051e38e16a86d0724a..f6cd1cdbd061312d3942e95e4fb712656e0233a3 100644 (file)
@@ -14,19 +14,23 @@ import { Permission } from '~/app/shared/models/permissions';
 
 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
 import { SmbService } from '~/app/shared/api/smb.service';
-import { SMBCluster } from '../smb.model';
+
 import { Icons } from '~/app/shared/enum/icons.enum';
 import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { URLBuilderService } from '~/app/shared/services/url-builder.service';
+import { SMBCluster } from '../smb.model';
 import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
 import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
 import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
 import { FinishedTask } from '~/app/shared/models/finished-task';
 
+const BASE_URL = 'cephfs/smb';
 @Component({
   selector: 'cd-smb-cluster-list',
   templateUrl: './smb-cluster-list.component.html',
-  styleUrls: ['./smb-cluster-list.component.scss']
+  styleUrls: ['./smb-cluster-list.component.scss'],
+  providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }]
 })
 export class SmbClusterListComponent extends ListWithDetails implements OnInit {
   @ViewChild('table', { static: true })
@@ -35,9 +39,9 @@ export class SmbClusterListComponent extends ListWithDetails implements OnInit {
   permission: Permission;
   tableActions: CdTableAction[];
   context: CdTableFetchDataContext;
+  selection = new CdTableSelection();
   smbClusters$: Observable<SMBCluster[]>;
   subject$ = new BehaviorSubject<SMBCluster[]>([]);
-  selection = new CdTableSelection();
   modalRef: NgbModalRef;
 
   constructor(
@@ -45,7 +49,8 @@ export class SmbClusterListComponent extends ListWithDetails implements OnInit {
     public actionLabels: ActionLabelsI18n,
     private smbService: SmbService,
     private modalService: ModalCdsService,
-    private taskWrapper: TaskWrapperService
+    private taskWrapper: TaskWrapperService,
+    private urlBuilder: URLBuilderService
   ) {
     super();
     this.permission = this.authStorageService.getPermissions().smb;
@@ -72,6 +77,16 @@ export class SmbClusterListComponent extends ListWithDetails implements OnInit {
         flexGrow: 2
       }
     ];
+    this.tableActions = [
+      {
+        name: `${this.actionLabels.CREATE}`,
+        permission: 'create',
+        icon: Icons.add,
+        routerLink: () => this.urlBuilder.getCreate(),
+
+        canBePrimary: (selection: CdTableSelection) => !selection.hasSingleSelection
+      }
+    ];
 
     this.smbClusters$ = this.subject$.pipe(
       switchMap(() =>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-domain-setting-modal/smb-domain-setting-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-domain-setting-modal/smb-domain-setting-modal.component.html
new file mode 100644 (file)
index 0000000..8d23a49
--- /dev/null
@@ -0,0 +1,111 @@
+<cds-modal size="md"
+           [open]="open"
+           [hasScrollingContent]="true"
+           (overlaySelected)="closeModal()">
+  <cds-modal-header (closeSelect)="closeModal()">
+    <h3 cdsModalHeaderHeading
+        i18n>{{ action | titlecase }} {{ resource | upperFirst }}</h3>
+  </cds-modal-header>
+  <ng-container *cdFormLoading="loading">
+    <form name="domainSettingsForm"
+          #formDir="ngForm"
+          [formGroup]="domainSettingsForm"
+          novalidate>
+      <div cdsModalContent>
+        <div class="form-item">
+          <cds-text-label
+            label="realm"
+            cdRequiredField="Realm Name"
+            [invalid]="
+              !domainSettingsForm.controls.realm.valid && domainSettingsForm.controls.realm.dirty
+            "
+            [invalidText]="realmNameError"
+            i18n
+            >Realm Name
+            <input
+              cdsText
+              type="text"
+              placeholder="Realm name..."
+              formControlName="realm"
+              autofocus
+            />
+          </cds-text-label>
+          <ng-template #realmNameError>
+            <span
+              *ngIf="domainSettingsForm.showError('realm', formDir, 'required')"
+              class="invalid-feedback"
+            >
+              <ng-container i18n> This field is required. </ng-container>
+            </span>
+          </ng-template>
+        </div>
+
+        <!-- Join Source -->
+        <ng-container
+          formArrayName="join_sources"
+          *ngFor="let joinSource of join_sources.controls; index as i"
+        >
+          <ng-container [formGroupName]="i">
+            <div cdsRow
+                 class="form-item form-item-append">
+              <div cdsCol
+                   [columnNumbers]="{ lg: 14 }">
+                <input
+                  cdsText
+                  type="text"
+                  placeholder="Id.."
+                  [id]="'ref' + i"
+                  formControlName="ref"
+                  modal-primary-focus
+                  [invalid]="
+                    !domainSettingsForm.controls['join_sources'].controls[i].valid &&
+                    domainSettingsForm.controls['join_sources'].dirty
+                  "
+                  [invalidText]="refError"
+                />
+                <ng-template #refError>
+                  <span
+                    class="invalid-feedback"
+                    *ngIf="domainSettingsForm.showError('join_sources', formDir, 'required')"
+                    i18n
+                    >This field is required.</span
+                  >
+                </ng-template>
+              </div>
+              <div cdsCol
+                   *ngIf="i > 0"
+                   [columnNumbers]="{ lg: 1 }">
+                <cds-icon-button kind="danger"
+                                 size="sm"
+                                 (click)="removeJoinSource(i)">
+                  <svg cdsIcon="trash-can"
+                       size="32"
+                       class="cds--btn__icon"></svg>
+                </cds-icon-button>
+              </div>
+            </div>
+          </ng-container>
+        </ng-container>
+        <div class="form-item">
+          <button cdsButton="tertiary"
+                  type="button"
+                  (click)="addJoinSource()"
+                  i18n>
+            Add Join Source
+            <svg cdsIcon="add"
+                 size="32"
+                 class="cds--btn__icon"
+                 icon></svg>
+          </button>
+        </div>
+      </div>
+      <cd-form-button-panel
+        (submitActionEvent)="submit()"
+        [form]="domainSettingsForm"
+        [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"
+        [modalForm]="true"
+      >
+      </cd-form-button-panel>
+    </form>
+  </ng-container>
+</cds-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-domain-setting-modal/smb-domain-setting-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-domain-setting-modal/smb-domain-setting-modal.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-domain-setting-modal/smb-domain-setting-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-domain-setting-modal/smb-domain-setting-modal.component.spec.ts
new file mode 100644 (file)
index 0000000..44a9ca5
--- /dev/null
@@ -0,0 +1,52 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SmbDomainSettingModalComponent } from './smb-domain-setting-modal.component';
+import { SharedModule } from '~/app/shared/shared.module';
+import { ToastrModule } from 'ngx-toastr';
+import { ReactiveFormsModule } from '@angular/forms';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+import { NgbActiveModal, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
+import { InputModule, ModalModule, SelectModule } from 'carbon-components-angular';
+
+describe('SmbDomainSettingModalComponent', () => {
+  let component: SmbDomainSettingModalComponent;
+  let fixture: ComponentFixture<SmbDomainSettingModalComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [SmbDomainSettingModalComponent],
+      imports: [
+        SharedModule,
+        ToastrModule.forRoot(),
+        ReactiveFormsModule,
+        HttpClientTestingModule,
+        RouterTestingModule,
+        NgbTypeaheadModule,
+        ModalModule,
+        InputModule,
+        SelectModule
+      ],
+      providers: [NgbActiveModal, { provide: 'domainSettingsObject', useValue: [[]] }]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(SmbDomainSettingModalComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('should add join sources', () => {
+    const defaultLength = component.join_sources.length;
+    component.addJoinSource();
+    expect(component.join_sources.length).toBe(defaultLength + 1);
+  });
+
+  it('should call submit', () => {
+    component.submit();
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-domain-setting-modal/smb-domain-setting-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-domain-setting-modal/smb-domain-setting-modal.component.ts
new file mode 100644 (file)
index 0000000..7a9cf10
--- /dev/null
@@ -0,0 +1,109 @@
+import { ChangeDetectorRef, Component, Inject, OnInit, Optional } from '@angular/core';
+import { FormArray, FormControl, FormGroup, UntypedFormControl, Validators } from '@angular/forms';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { RgwRealmService } from '~/app/shared/api/rgw-realm.service';
+import { SmbService } from '~/app/shared/api/smb.service';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import { DomainSettings } from '../smb.model';
+
+@Component({
+  selector: 'cd-smb-domain-setting-modal',
+  templateUrl: './smb-domain-setting-modal.component.html',
+  styleUrls: ['./smb-domain-setting-modal.component.scss']
+})
+export class SmbDomainSettingModalComponent extends CdForm implements OnInit {
+  domainSettingsForm: CdFormGroup;
+  realmNames: string[];
+
+  constructor(
+    public activeModal: NgbActiveModal,
+    public actionLabels: ActionLabelsI18n,
+    public rgwRealmService: RgwRealmService,
+    public notificationService: NotificationService,
+    public smbService: SmbService,
+    private cd: ChangeDetectorRef,
+    @Optional() @Inject('action') public action: string,
+    @Optional() @Inject('resource') public resource: string,
+    @Optional()
+    @Inject('domainSettingsObject')
+    public domainSettingsObject?: DomainSettings
+  ) {
+    super();
+    this.action = this.actionLabels.UPDATE;
+    this.resource = $localize`Domain Setting`;
+  }
+
+  private createForm() {
+    this.domainSettingsForm = new CdFormGroup({
+      realm: new UntypedFormControl('', {
+        validators: [
+          Validators.required,
+          CdValidators.custom('uniqueName', (realm: string) => {
+            return this.realmNames && this.realmNames.indexOf(realm) !== -1;
+          })
+        ]
+      }),
+      join_sources: new FormArray([])
+    });
+  }
+
+  ngOnInit(): void {
+    this.createForm();
+    this.loadingReady();
+    this.domainSettingsForm.get('realm').setValue(this.domainSettingsObject?.realm);
+    const join_sources = this.domainSettingsForm.get('join_sources') as FormArray;
+
+    if (this.domainSettingsObject?.join_sources) {
+      this.domainSettingsObject.join_sources.forEach((source: { ref: string }) => {
+        join_sources.push(
+          new FormGroup({
+            ref: new FormControl(source.ref || '', Validators.required)
+          })
+        );
+      });
+    }
+
+    if (!this.domainSettingsObject) {
+      this.join_sources.push(
+        new FormGroup({
+          ref: new FormControl('', Validators.required)
+        })
+      );
+    } else {
+      this.action = this.actionLabels.EDIT;
+    }
+  }
+
+  submit() {
+    this.smbService.passData(this.domainSettingsForm.value);
+    this.closeModal();
+  }
+
+  get join_sources() {
+    return this.domainSettingsForm.get('join_sources') as FormArray;
+  }
+
+  addJoinSource() {
+    this.join_sources.push(
+      new FormGroup({
+        ref: new FormControl('', Validators.required)
+      })
+    );
+    this.cd.detectChanges();
+  }
+
+  removeJoinSource(index: number) {
+    const join_sources = this.domainSettingsForm.get('join_sources') as FormArray;
+
+    if (index >= 0 && index < join_sources.length) {
+      join_sources.removeAt(index);
+    }
+
+    this.cd.detectChanges();
+  }
+}
index 3796d924565ab06d1d6d81195ac0885d93ed5631..a5e10490a7b9426a6a32709ccaa8655a0fdb0e32 100644 (file)
@@ -1,20 +1,29 @@
 import { CephServicePlacement } from '~/app/shared/models/service.interface';
 
 export interface SMBCluster {
+  resource_type: string;
   cluster_id: string;
-  auth_mode: AuthMode;
-  intent: string;
+  auth_mode: typeof AUTHMODE;
   domain_settings?: DomainSettings;
-  user_group_settings?: string[];
+  user_group_settings?: JoinSource[];
   custom_dns?: string[];
   placement?: CephServicePlacement;
-  clustering?: string;
+  clustering?: typeof CLUSTERING;
   public_addrs?: PublicAddress;
 }
 
+export interface RequestModel {
+  cluster_resource: SMBCluster;
+}
+
 export interface DomainSettings {
   realm?: string;
-  join_sources_ref?: string[];
+  join_sources?: JoinSource[];
+}
+
+export interface JoinSource {
+  source_type: string;
+  ref: string;
 }
 
 export interface PublicAddress {
@@ -22,7 +31,25 @@ export interface PublicAddress {
   destination: string;
 }
 
-export interface AuthMode {
-  user: 'User';
-  activeDirectory: 'active-directory';
-}
+export const CLUSTERING = {
+  Default: 'default',
+  Always: 'always',
+  Never: 'never'
+};
+
+export const RESOURCE = {
+  ClusterResource: 'cluster_resource',
+  Resource: 'resource'
+};
+
+export const AUTHMODE = {
+  User: 'user',
+  activeDirectory: 'active-directory'
+};
+
+export const PLACEMENT = {
+  host: 'hosts',
+  label: 'label'
+};
+
+export const RESOURCE_TYPE = 'ceph.smb.cluster';
index 7cd237dd8e0754fc3c0d379e679ec4686b7929d3..9caadd5a97cdf64bb37830d2fc066967eb3eb489 100644 (file)
@@ -1,41 +1,59 @@
-import { CommonModule } from '@angular/common';
-import { NgModule } from '@angular/core';
-import { ReactiveFormsModule } from '@angular/forms';
-import { RouterModule } from '@angular/router';
-
-import { NgbNavModule, NgbTooltipModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
-
-import { SharedModule } from '~/app/shared/shared.module';
-
+import Close from '@carbon/icons/es/close/32';
+import { SmbClusterListComponent } from './smb-cluster-list/smb-cluster-list.component';
+import { SmbClusterFormComponent } from './smb-cluster-form/smb-cluster-form.component';
+import { AppRoutingModule } from '~/app/app-routing.module';
+import { NgChartsModule } from 'ng2-charts';
+import { DataTableModule } from '~/app/shared/datatable/datatable.module';
+import { SmbDomainSettingModalComponent } from './smb-domain-setting-modal/smb-domain-setting-modal.component';
 import {
   ButtonModule,
+  CheckboxModule,
+  ComboBoxModule,
+  DropdownModule,
   GridModule,
   IconModule,
   IconService,
   InputModule,
+  LayoutModule,
+  ModalModule,
+  NumberModule,
+  PlaceholderModule,
   SelectModule
 } from 'carbon-components-angular';
-
-import Close from '@carbon/icons/es/close/32';
-import { SmbClusterListComponent } from './smb-cluster-list/smb-cluster-list.component';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { CommonModule } from '@angular/common';
+import { SharedModule } from '~/app/shared/shared.module';
+import { RouterModule } from '@angular/router';
+import { NgModule } from '@angular/core';
 
 @NgModule({
   imports: [
     ReactiveFormsModule,
     RouterModule,
+    CommonModule,
     SharedModule,
-    NgbNavModule,
+    AppRoutingModule,
+    NgChartsModule,
     CommonModule,
-    NgbTypeaheadModule,
-    NgbTooltipModule,
+    FormsModule,
+    ReactiveFormsModule,
+    DataTableModule,
     GridModule,
     SelectModule,
     InputModule,
+    CheckboxModule,
+    SelectModule,
+    DropdownModule,
+    ModalModule,
+    PlaceholderModule,
     ButtonModule,
+    NumberModule,
+    LayoutModule,
+    ComboBoxModule,
     IconModule
   ],
-  exports: [SmbClusterListComponent],
-  declarations: [SmbClusterListComponent]
+  exports: [SmbClusterListComponent, SmbClusterFormComponent],
+  declarations: [SmbClusterListComponent, SmbClusterFormComponent, SmbDomainSettingModalComponent]
 })
 export class SmbModule {
   constructor(private iconService: IconService) {
index f20977330d14193ad78d49d3867eff2818354ed5..e7dc64520f939391e9afe2b604aaec86f41fb0dc 100644 (file)
@@ -29,6 +29,12 @@ describe('SmbService', () => {
     expect(req.request.method).toBe('GET');
   });
 
+  it('should call create', () => {
+    service.createCluster('test').subscribe();
+    const req = httpTesting.expectOne('api/smb/cluster');
+    expect(req.request.method).toBe('POST');
+  });
+
   it('should call remove', () => {
     service.removeCluster('cluster_1').subscribe();
     const req = httpTesting.expectOne('api/smb/cluster/cluster_1');
index ba640d11451f73a9702aee8f4af0681a9b35b9cc..b5e8007482bf3f0bab6b3f070557471ada9b8d48 100644 (file)
@@ -1,21 +1,31 @@
 import { HttpClient } from '@angular/common/http';
 import { Injectable } from '@angular/core';
-import { Observable } from 'rxjs';
+import { Observable, Subject } from 'rxjs';
 
-import { SMBCluster } from '~/app/ceph/smb/smb.model';
+import { DomainSettings, SMBCluster } from '~/app/ceph/smb/smb.model';
 
 @Injectable({
   providedIn: 'root'
 })
 export class SmbService {
   baseURL = 'api/smb';
+  private modalDataSubject = new Subject<DomainSettings>();
+  modalData$ = this.modalDataSubject.asObservable();
 
   constructor(private http: HttpClient) {}
 
+  passData(data: DomainSettings) {
+    this.modalDataSubject.next(data);
+  }
+
   listClusters(): Observable<SMBCluster[]> {
     return this.http.get<SMBCluster[]>(`${this.baseURL}/cluster`);
   }
 
+  createCluster(requestModel: any) {
+    return this.http.post(`${this.baseURL}/cluster`, requestModel);
+  }
+
   removeCluster(clusterId: string) {
     return this.http.delete(`${this.baseURL}/cluster/${clusterId}`, {
       observe: 'response'
index f07e85a07951a966fec9d53dca86da6012d3ad2e..f906a25ad2a9921ca73b075e281525143cc0a419 100644 (file)
@@ -65,8 +65,8 @@ export interface CephServiceAdditionalSpec {
 }
 
 export interface CephServicePlacement {
-  count: number;
-  placement: string;
-  hosts: string[];
-  label: string;
+  count?: number;
+  placement?: string;
+  hosts?: string[];
+  label?: string;
 }
index e0711ead95d88d373e0a2314f99b6ba34658c445..0fe76a5dfde1ce80169d63bc7f54029d01bb9ed6 100644 (file)
@@ -398,7 +398,7 @@ export class TaskMessageService {
     ),
     // smb
     'smb/cluster/remove': this.newTaskMessage(this.commonOperations.remove, (metadata) =>
-      this.smb(metadata)
+      this.smbCluster(metadata)
     ),
     // Grafana tasks
     'grafana/dashboards/update': this.newTaskMessage(
@@ -483,6 +483,10 @@ export class TaskMessageService {
     'cephfs/snapshot/schedule/deactivate': this.newTaskMessage(
       this.commonOperations.deactivate,
       (metadata) => this.snapshotSchedule(metadata)
+    ),
+    // smb
+    'smb/cluster/create': this.newTaskMessage(this.commonOperations.create, (metadata) =>
+      this.smbCluster(metadata)
     )
   };
 
@@ -543,8 +547,8 @@ export class TaskMessageService {
     }'`;
   }
 
-  smb(metadata: { cluster_id: string }) {
-    return $localize`SMB Cluster '${metadata.cluster_id}'`;
+  smbCluster(metadata: any) {
+    return $localize`SMB Cluster  '${metadata.cluster_id}'`;
   }
 
   service(metadata: any) {