From 3571c6dd5ff3dc311a7fb3c0cadc0f832fe906c9 Mon Sep 17 00:00:00 2001 From: Dnyaneshwari Date: Tue, 19 Nov 2024 12:31:52 +0530 Subject: [PATCH] mgr/dashboard: create smb cluster Fixes: https://tracker.ceph.com/issues/69156 Signed-off-by: Dnyaneshwari Talwekar --- .../frontend/src/app/app-routing.module.ts | 10 +- .../rbd-trash-move-modal.component.spec.ts | 2 +- ...ephfs-snapshotschedule-form.component.html | 2 +- .../smb-cluster-form.component.html | 330 ++++++++++++++++++ .../smb-cluster-form.component.scss | 0 .../smb-cluster-form.component.spec.ts | 91 +++++ .../smb-cluster-form.component.ts | 309 ++++++++++++++++ .../smb-cluster-list.component.html | 18 +- .../smb-cluster-list.component.ts | 23 +- .../smb-domain-setting-modal.component.html | 111 ++++++ .../smb-domain-setting-modal.component.scss | 0 ...smb-domain-setting-modal.component.spec.ts | 52 +++ .../smb-domain-setting-modal.component.ts | 109 ++++++ .../frontend/src/app/ceph/smb/smb.model.ts | 45 ++- .../frontend/src/app/ceph/smb/smb.module.ts | 52 ++- .../src/app/shared/api/smb.service.spec.ts | 6 + .../src/app/shared/api/smb.service.ts | 14 +- .../app/shared/models/service.interface.ts | 8 +- .../shared/services/task-message.service.ts | 10 +- 19 files changed, 1140 insertions(+), 52 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-domain-setting-modal/smb-domain-setting-modal.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-domain-setting-modal/smb-domain-setting-modal.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-domain-setting-modal/smb-domain-setting-modal.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-domain-setting-modal/smb-domain-setting-modal.component.ts diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts index 89b2de5513f..f7a139e5b9d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts @@ -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 } + } + ] } ] }, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-move-modal/rbd-trash-move-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-move-modal/rbd-trash-move-modal.component.spec.ts index efee2aacfba..3816601f781 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-move-modal/rbd-trash-move-modal.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-move-modal/rbd-trash-move-modal.component.spec.ts @@ -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); }); }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-form/cephfs-snapshotschedule-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-form/cephfs-snapshotschedule-form.component.html index 1a611dc18d7..f1a2dcb6b4b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-form/cephfs-snapshotschedule-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-form/cephfs-snapshotschedule-form.component.html @@ -126,7 +126,7 @@
- +
+
+ {{ action | titlecase }} {{ resource | upperFirst }} +
+ + +
+ Cluster Name + + + + This field is required. + +
+ + +
+ + + + + + This field is required. + +
+ + +
+
+ Domain Settings +
+ + + + + + + + +
+
+
+ Specify the Realm and Join Sources in the Domain Settings field. +
+
+ + + +
+
+ User Group Id + + + + This field is required. + +
+
+ + + +
+
+
+ +
+ +
+ + + +
+ + + + +
+ + +
+ + + + + This field is required. + +
+ + +
+ + + +
+
+
+ +
+ +
+ + +
+ + + +
+ + + +
+
+ +
+
+ + + +
+
+
+ +
+ +
+ + +
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 index 00000000000..e69de29bb2d 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 index 00000000000..73bc10c4685 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.spec.ts @@ -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; + + 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 index 00000000000..1acbb8efec3 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-form/smb-cluster-form.component.ts @@ -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; + 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); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-list/smb-cluster-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-list/smb-cluster-list.component.html index cfd7b65dbf4..8d3fa098ad5 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-list/smb-cluster-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-list/smb-cluster-list.component.html @@ -12,14 +12,12 @@ (fetchData)="loadSMBCluster($event)" (updateSelection)="updateSelection($event)" > -
- - -
- +
+ + +
+ diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-list/smb-cluster-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-list/smb-cluster-list.component.ts index 018e685d111..f6cd1cdbd06 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-list/smb-cluster-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-list/smb-cluster-list.component.ts @@ -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; subject$ = new BehaviorSubject([]); - 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 index 00000000000..8d23a4919a8 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-domain-setting-modal/smb-domain-setting-modal.component.html @@ -0,0 +1,111 @@ + + +

{{ action | titlecase }} {{ resource | upperFirst }}

+
+ +
+
+
+ Realm Name + + + + + This field is required. + + +
+ + + + +
+
+ + + This field is required. + +
+
+ + + +
+
+
+
+
+ +
+
+ + +
+
+
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 index 00000000000..e69de29bb2d 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 index 00000000000..44a9ca555d5 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-domain-setting-modal/smb-domain-setting-modal.component.spec.ts @@ -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; + + 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 index 00000000000..7a9cf1033d4 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-domain-setting-modal/smb-domain-setting-modal.component.ts @@ -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(); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.model.ts index 3796d924565..a5e10490a7b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.model.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.model.ts @@ -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'; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.module.ts index 7cd237dd8e0..9caadd5a97c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.module.ts @@ -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) { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.spec.ts index f20977330d1..e7dc64520f9 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.spec.ts @@ -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'); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.ts index ba640d11451..b5e8007482b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.ts @@ -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(); + modalData$ = this.modalDataSubject.asObservable(); constructor(private http: HttpClient) {} + passData(data: DomainSettings) { + this.modalDataSubject.next(data); + } + listClusters(): Observable { return this.http.get(`${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' diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/service.interface.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/service.interface.ts index f07e85a0795..f906a25ad2a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/service.interface.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/service.interface.ts @@ -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; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts index e0711ead95d..0fe76a5dfde 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts @@ -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) { -- 2.39.5