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 {
},
breadcrumbs: 'File/SMB'
},
- children: [{ path: '', component: SmbClusterListComponent }]
+ children: [
+ { path: '', component: SmbClusterListComponent },
+ {
+ path: `${URLVerbs.CREATE}`,
+ component: SmbClusterFormComponent,
+ data: { breadcrumbs: ActionLabels.CREATE }
+ }
+ ]
}
]
},
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);
});
});
});
<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"
--- /dev/null
+<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>
--- /dev/null
+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();
+ });
+});
--- /dev/null
+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);
+ }
+}
(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>
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 })
permission: Permission;
tableActions: CdTableAction[];
context: CdTableFetchDataContext;
+ selection = new CdTableSelection();
smbClusters$: Observable<SMBCluster[]>;
subject$ = new BehaviorSubject<SMBCluster[]>([]);
- selection = new CdTableSelection();
modalRef: NgbModalRef;
constructor(
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;
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(() =>
--- /dev/null
+<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>
--- /dev/null
+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();
+ });
+});
--- /dev/null
+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();
+ }
+}
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 {
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';
-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) {
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');
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'
}
export interface CephServicePlacement {
- count: number;
- placement: string;
- hosts: string[];
- label: string;
+ count?: number;
+ placement?: string;
+ hosts?: string[];
+ label?: string;
}
),
// smb
'smb/cluster/remove': this.newTaskMessage(this.commonOperations.remove, (metadata) =>
- this.smb(metadata)
+ this.smbCluster(metadata)
),
// Grafana tasks
'grafana/dashboards/update': this.newTaskMessage(
'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)
)
};
}'`;
}
- 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) {