From 7716c9b4333bb7a0087bba7163661dc2a8e63863 Mon Sep 17 00:00:00 2001 From: Tiago Melo Date: Thu, 4 Oct 2018 17:24:20 +0100 Subject: [PATCH] mgr/dashboard: Add UI for NFS Form Signed-off-by: Tiago Melo --- .../nfs-form-client.component.html | 104 ++++ .../nfs-form-client.component.scss | 0 .../nfs-form-client.component.spec.ts | 72 +++ .../nfs-form-client.component.ts | 88 +++ .../ceph/nfs/nfs-form/nfs-form.component.html | 525 ++++++++++++++++ .../ceph/nfs/nfs-form/nfs-form.component.scss | 3 + .../nfs/nfs-form/nfs-form.component.spec.ts | 204 +++++++ .../ceph/nfs/nfs-form/nfs-form.component.ts | 558 ++++++++++++++++++ .../src/app/shared/pipes/ordinal.pipe.spec.ts | 8 + .../src/app/shared/pipes/ordinal.pipe.ts | 25 + .../src/app/shared/pipes/pipes.module.ts | 10 +- .../frontend/src/locale/messages.xlf | 545 ++++++++++++++++- 12 files changed, 2134 insertions(+), 8 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ordinal.pipe.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ordinal.pipe.ts diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.html new file mode 100644 index 0000000000000..fa75da44af4e9 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.html @@ -0,0 +1,104 @@ +
+ + +
+ + Any client can access + + + +
+
+
+

{{ (index + 1) | ordinal }} + × +

+
+ +
+ +
+ +
+ + + Required field + + + Must contain one or more comma-separated values +
+ For example: 192.168.0.10, 192.168.1.0/8 +
+
+
+
+ + +
+ +
+ + + {{ getAccessTypeHelp(index) }} + +
+
+ + +
+ +
+ +
+
+
+
+
+
+ + + + +
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.spec.ts new file mode 100644 index 0000000000000..3c8e0bb502de7 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.spec.ts @@ -0,0 +1,72 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; + +import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper'; +import { CdFormBuilder } from '../../../shared/forms/cd-form-builder'; +import { CdFormGroup } from '../../../shared/forms/cd-form-group'; +import { SharedModule } from '../../../shared/shared.module'; +import { NfsFormClientComponent } from './nfs-form-client.component'; + +describe('NfsFormClientComponent', () => { + let component: NfsFormClientComponent; + let fixture: ComponentFixture; + + configureTestBed({ + declarations: [NfsFormClientComponent], + imports: [ReactiveFormsModule, SharedModule, HttpClientTestingModule], + providers: i18nProviders + }); + + beforeEach(() => { + fixture = TestBed.createComponent(NfsFormClientComponent); + const formBuilder = TestBed.get(CdFormBuilder); + component = fixture.componentInstance; + + component.form = this.nfsForm = new CdFormGroup({ + access_type: new FormControl(''), + clients: formBuilder.array([]), + squash: new FormControl('') + }); + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should add a client', () => { + expect(component.form.getValue('clients')).toEqual([]); + component.addClient(); + expect(component.form.getValue('clients')).toEqual([ + { access_type: '', addresses: '', squash: '' } + ]); + }); + + it('should return form access_type', () => { + expect(component.getNoAccessTypeDescr()).toBe('-- Select the access type --'); + + component.form.patchValue({ access_type: 'RW' }); + expect(component.getNoAccessTypeDescr()).toBe('RW (inherited from global config)'); + }); + + it('should return form squash', () => { + expect(component.getNoSquashDescr()).toBe( + '-- Select what kind of user id squashing is performed --' + ); + + component.form.patchValue({ squash: 'root_id_squash' }); + expect(component.getNoSquashDescr()).toBe('root_id_squash (inherited from global config)'); + }); + + it('should remove client', () => { + component.addClient(); + expect(component.form.getValue('clients')).toEqual([ + { access_type: '', addresses: '', squash: '' } + ]); + + component.removeClient(0); + expect(component.form.getValue('clients')).toEqual([]); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.ts new file mode 100644 index 0000000000000..986dd5a4f6d23 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.ts @@ -0,0 +1,88 @@ +import { Component, Input } from '@angular/core'; +import { FormArray, FormControl, Validators } from '@angular/forms'; + +import { I18n } from '@ngx-translate/i18n-polyfill'; +import * as _ from 'lodash'; + +import { NfsService } from '../../../shared/api/nfs.service'; +import { CdFormGroup } from '../../../shared/forms/cd-form-group'; + +@Component({ + selector: 'cd-nfs-form-client', + templateUrl: './nfs-form-client.component.html', + styleUrls: ['./nfs-form-client.component.scss'] +}) +export class NfsFormClientComponent { + @Input() + form: CdFormGroup; + + nfsSquash: any[] = this.nfsService.nfsSquash; + nfsAccessType: any[] = this.nfsService.nfsAccessType; + + constructor(private nfsService: NfsService, private i18n: I18n) {} + + getNoAccessTypeDescr() { + if (this.form.getValue('access_type')) { + return `${this.form.getValue('access_type')} ${this.i18n('(inherited from global config)')}`; + } + return this.i18n('-- Select the access type --'); + } + + getAccessTypeHelp(index) { + const accessTypeItem = this.nfsAccessType.find((currentAccessTypeItem) => { + return this.getValue(index, 'access_type') === currentAccessTypeItem.value; + }); + return _.isObjectLike(accessTypeItem) ? accessTypeItem.help : ''; + } + + getNoSquashDescr() { + if (this.form.getValue('squash')) { + return `${this.form.getValue('squash')} (${this.i18n('inherited from global config')})`; + } + return this.i18n('-- Select what kind of user id squashing is performed --'); + } + + addClient() { + const clients = this.form.get('clients') as FormArray; + + const REGEX_IP = `(([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\.([0-9]{1,3})([/](\\d|[1-2]\\d|3[0-2]))?)`; + const REGEX_LIST_IP = `${REGEX_IP}([ ,]{1,2}${REGEX_IP})*`; + + const fg = new CdFormGroup({ + addresses: new FormControl('', { + validators: [Validators.required, Validators.pattern(REGEX_LIST_IP)] + }), + access_type: new FormControl(this.form.getValue('access_type')), + squash: new FormControl(this.form.getValue('squash')) + }); + + clients.push(fg); + return fg; + } + + removeClient(index) { + const clients = this.form.get('clients') as FormArray; + clients.removeAt(index); + } + + showError(index, control, formDir, x) { + return (this.form.controls.clients).controls[index].showError(control, formDir, x); + } + + getValue(index, control) { + const clients = this.form.get('clients') as FormArray; + const client = clients.at(index) as CdFormGroup; + return client.getValue(control); + } + + resolveModel(clients: any[]) { + _.forEach(clients, (client) => { + const fg = this.addClient(); + fg.patchValue(client); + }); + } + + trackByFn(index) { + return index; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.html new file mode 100644 index 0000000000000..d67ddd7cf6292 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.html @@ -0,0 +1,525 @@ +
+
+
+
+

NFS export {{ export_id ? cluster_id + ':' + export_id : '' }}

+
+ +
+ + +
+ +
+ + Required field +
+
+ + +
+ +
+ +
+ + + + +
+
+ +
+
+ + + Add daemon + +
+
+
+
+ + +
+ +
+ +
+ + Required field +
+
+ + +
+ +
+ + Required field +
+
+ + +
+ +
+ + Required field +
+
+ + +
+ +
+ + Required field +
+
+
+ + +
+ + +
+
+ + +
+ +
+ + + + Required field +
+
+ + +
+ +
+ + Required field + + Path need to start with a '/' and can be followed by a word + New directory will be created +
+
+ + +
+ +
+ + + Required field + + Path can only be a single '/' or a word + + New bucket will be created +
+
+ + +
+ +
+
+ + +
+
+ + +
+ Required field +
+
+ + +
+ +
+ +
+
+ + +
+ +
+ + Required field + Wrong format +
+
+ + +
+ +
+ + + {{ getAccessTypeHelp(nfsForm.getValue('access_type')) }} + + Required field +
+
+ + +
+ +
+ + Required field +
+
+ + +
+ +
+
+ + +
+
+ + +
+ Required field +
+
+
+ + + + + +
+ + +
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.scss new file mode 100644 index 0000000000000..cebcc8877a217 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.scss @@ -0,0 +1,3 @@ +.cd-mb { + margin-bottom: 10px; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.spec.ts new file mode 100644 index 0000000000000..b5bf6a779b0e2 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.spec.ts @@ -0,0 +1,204 @@ +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { ToastModule } from 'ng2-toastr'; +import { TypeaheadModule } from 'ngx-bootstrap/typeahead'; + +import { ActivatedRouteStub } from '../../../../testing/activated-route-stub'; +import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper'; +import { SummaryService } from '../../../shared/services/summary.service'; +import { SharedModule } from '../../../shared/shared.module'; +import { NfsFormClientComponent } from '../nfs-form-client/nfs-form-client.component'; +import { NfsFormComponent } from './nfs-form.component'; + +describe('NfsFormComponent', () => { + let component: NfsFormComponent; + let fixture: ComponentFixture; + let httpTesting: HttpTestingController; + let activatedRoute: ActivatedRouteStub; + + configureTestBed( + { + declarations: [NfsFormComponent, NfsFormClientComponent], + imports: [ + HttpClientTestingModule, + ReactiveFormsModule, + RouterTestingModule, + SharedModule, + ToastModule.forRoot(), + TypeaheadModule.forRoot() + ], + providers: [ + { + provide: ActivatedRoute, + useValue: new ActivatedRouteStub({ cluster_id: undefined, export_id: undefined }) + }, + i18nProviders + ] + }, + true + ); + + beforeEach(() => { + const summaryService = TestBed.get(SummaryService); + spyOn(summaryService, 'refresh').and.callFake(() => true); + + fixture = TestBed.createComponent(NfsFormComponent); + component = fixture.componentInstance; + httpTesting = TestBed.get(HttpTestingController); + activatedRoute = TestBed.get(ActivatedRoute); + fixture.detectChanges(); + + httpTesting.expectOne('api/summary').flush([]); + httpTesting + .expectOne('api/nfs-ganesha/daemon') + .flush([ + { daemon_id: 'node1', cluster_id: 'cluster1' }, + { daemon_id: 'node2', cluster_id: 'cluster1' }, + { daemon_id: 'node5', cluster_id: 'cluster2' } + ]); + httpTesting.expectOne('ui-api/nfs-ganesha/fsals').flush(['CEPH', 'RGW']); + httpTesting.expectOne('ui-api/nfs-ganesha/cephx/clients').flush(['admin', 'fs', 'rgw']); + httpTesting.expectOne('ui-api/nfs-ganesha/cephfs/filesystems').flush([{ id: 1, name: 'a' }]); + httpTesting.expectOne('api/rgw/user').flush(['test', 'dev']); + httpTesting.expectOne('api/rgw/user/dev').flush({ suspended: 0, user_id: 'dev', keys: ['a'] }); + httpTesting + .expectOne('api/rgw/user/test') + .flush({ suspended: 1, user_id: 'test', keys: ['a'] }); + httpTesting.verify(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should process all data', () => { + expect(component.allDaemons).toEqual({ cluster1: ['node1', 'node2'], cluster2: ['node5'] }); + expect(component.isDefaultCluster).toEqual(false); + expect(component.allFsals).toEqual([ + { descr: 'CephFS', value: 'CEPH' }, + { descr: 'Object Gateway', value: 'RGW' } + ]); + expect(component.allCephxClients).toEqual(['admin', 'fs', 'rgw']); + expect(component.allFsNames).toEqual([{ id: 1, name: 'a' }]); + expect(component.allRgwUsers).toEqual(['dev']); + }); + + it('should create the form', () => { + expect(component.nfsForm.value).toEqual({ + access_type: 'RW', + clients: [], + cluster_id: '', + daemons: [], + fsal: { fs_name: 'a', name: '', rgw_user_id: '', user_id: '' }, + path: '', + protocolNfsv3: true, + protocolNfsv4: true, + pseudo: '', + sec_label_xattr: 'security.selinux', + security_label: false, + squash: 'None', + tag: '', + transportTCP: true, + transportUDP: true + }); + }); + + it('should prepare data when selecting an cluster', () => { + expect(component.allDaemons).toEqual({ cluster1: ['node1', 'node2'], cluster2: ['node5'] }); + expect(component.daemonsSelections).toEqual([]); + + component.nfsForm.patchValue({ cluster_id: 'cluster1' }); + component.onClusterChange(); + + expect(component.daemonsSelections).toEqual([ + { description: '', name: 'node1', selected: false }, + { description: '', name: 'node2', selected: false } + ]); + }); + + it('should clean data when changing cluster', () => { + component.nfsForm.patchValue({ cluster_id: 'cluster1', daemons: ['node1'] }); + component.nfsForm.patchValue({ cluster_id: 'node2' }); + component.onClusterChange(); + + expect(component.nfsForm.getValue('daemons')).toEqual([]); + }); + + describe('should submit request', () => { + beforeEach(() => { + component.nfsForm.patchValue({ + access_type: 'RW', + clients: [], + cluster_id: 'cluster1', + daemons: ['node2'], + fsal: { name: 'CEPH', user_id: 'fs', fs_name: 1, rgw_user_id: '' }, + path: '/foo', + protocolNfsv3: true, + protocolNfsv4: true, + pseudo: '/baz', + squash: 'no_root_squash', + tag: 'bar', + transportTCP: true, + transportUDP: true + }); + }); + + it('should call update', () => { + activatedRoute.setParams({ cluster_id: 'cluster1', export_id: '1' }); + component.isEdit = true; + component.cluster_id = 'cluster1'; + component.export_id = '1'; + component.nfsForm.patchValue({ export_id: 1 }); + component.submitAction(); + + const req = httpTesting.expectOne('api/nfs-ganesha/export/cluster1/1'); + expect(req.request.method).toBe('PUT'); + expect(req.request.body).toEqual({ + access_type: 'RW', + clients: [], + cluster_id: 'cluster1', + daemons: ['node2'], + export_id: '1', + fsal: { fs_name: 1, name: 'CEPH', sec_label_xattr: null, user_id: 'fs' }, + path: '/foo', + protocols: [3, 4], + pseudo: '/baz', + security_label: false, + squash: 'no_root_squash', + tag: 'bar', + transports: ['TCP', 'UDP'] + }); + }); + + it('should call create', () => { + activatedRoute.setParams({ cluster_id: undefined, export_id: undefined }); + component.submitAction(); + + const req = httpTesting.expectOne('api/nfs-ganesha/export'); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ + access_type: 'RW', + clients: [], + cluster_id: 'cluster1', + daemons: ['node2'], + fsal: { + fs_name: 1, + name: 'CEPH', + sec_label_xattr: null, + user_id: 'fs' + }, + path: '/foo', + protocols: [3, 4], + pseudo: '/baz', + security_label: false, + squash: 'no_root_squash', + tag: 'bar', + transports: ['TCP', 'UDP'] + }); + }); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.ts new file mode 100644 index 0000000000000..2ab70555b639b --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.ts @@ -0,0 +1,558 @@ +import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { I18n } from '@ngx-translate/i18n-polyfill'; +import * as _ from 'lodash'; +import { forkJoin, Observable, of } from 'rxjs'; +import { map, mergeMap } from 'rxjs/operators'; + +import { NfsService } from '../../../shared/api/nfs.service'; +import { RgwUserService } from '../../../shared/api/rgw-user.service'; +import { SelectMessages } from '../../../shared/components/select/select-messages.model'; +import { SelectOption } from '../../../shared/components/select/select-option.model'; +import { CdFormBuilder } from '../../../shared/forms/cd-form-builder'; +import { CdFormGroup } from '../../../shared/forms/cd-form-group'; +import { CdValidators } from '../../../shared/forms/cd-validators'; +import { FinishedTask } from '../../../shared/models/finished-task'; +import { Permission } from '../../../shared/models/permissions'; +import { AuthStorageService } from '../../../shared/services/auth-storage.service'; +import { TaskWrapperService } from '../../../shared/services/task-wrapper.service'; +import { NfsFormClientComponent } from '../nfs-form-client/nfs-form-client.component'; + +@Component({ + selector: 'cd-nfs-form', + templateUrl: './nfs-form.component.html', + styleUrls: ['./nfs-form.component.scss'] +}) +export class NfsFormComponent implements OnInit { + @ViewChild('nfsClients') + nfsClients: NfsFormClientComponent; + + permission: Permission; + nfsForm: CdFormGroup; + isEdit = false; + + cluster_id = null; + export_id = null; + + isNewDirectory = false; + isNewBucket = false; + isDefaultCluster = false; + + allClusters: string[] = null; + allDaemons = {}; + + allFsals: any[] = []; + allRgwUsers: any[] = []; + allCephxClients: any[] = null; + allFsNames: any[] = null; + + nfsAccessType: any[] = this.nfsService.nfsAccessType; + nfsSquash: any[] = this.nfsService.nfsSquash; + + daemonsSelections: SelectOption[] = []; + daemonsMessages = new SelectMessages( + { noOptions: this.i18n('There are no daemons available.') }, + this.i18n + ); + + pathDataSource: Observable = Observable.create((observer: any) => { + observer.next(this.nfsForm.getValue('path')); + }).pipe( + mergeMap((token: string) => this.getPathTypeahead(token)), + map((val: any) => val.paths) + ); + + bucketDataSource: Observable = Observable.create((observer: any) => { + observer.next(this.nfsForm.getValue('path')); + }).pipe(mergeMap((token: string) => this.getBucketTypeahead(token))); + + constructor( + private authStorageService: AuthStorageService, + private nfsService: NfsService, + private route: ActivatedRoute, + private router: Router, + private rgwUserService: RgwUserService, + private formBuilder: CdFormBuilder, + private taskWrapper: TaskWrapperService, + private cdRef: ChangeDetectorRef, + private i18n: I18n + ) { + this.permission = this.authStorageService.getPermissions().pool; + this.createForm(); + } + + ngOnInit() { + const promises: any[] = [ + this.nfsService.daemon(), + this.nfsService.fsals(), + this.nfsService.clients(), + this.nfsService.filesystems() + ]; + + if (this.router.url.startsWith('/nfs/edit')) { + this.isEdit = true; + } + + if (this.isEdit) { + this.route.params.subscribe((params: { cluster_id: string; export_id: string }) => { + this.cluster_id = decodeURIComponent(params.cluster_id); + this.export_id = decodeURIComponent(params.export_id); + promises.push(this.nfsService.get(this.cluster_id, this.export_id)); + + this.getData(promises); + }); + } else { + this.getData(promises); + } + } + + getData(promises) { + forkJoin(promises).subscribe( + (data: any[]) => { + this.resolveDaemons(data[0]); + this.resolvefsals(data[1]); + this.resolveClients(data[2]); + this.resolveFilesystems(data[3]); + if (data[4]) { + this.resolveModel(data[4]); + } + }, + (error) => { + // this.error = error; + } + ); + } + + createForm() { + this.nfsForm = new CdFormGroup({ + cluster_id: new FormControl('', { + validators: [Validators.required] + }), + daemons: new FormControl([]), + fsal: new CdFormGroup({ + name: new FormControl('', { + validators: [Validators.required] + }), + user_id: new FormControl('', { + validators: [ + CdValidators.requiredIf({ + name: 'CEPH' + }) + ] + }), + fs_name: new FormControl('', { + validators: [ + CdValidators.requiredIf({ + name: 'CEPH' + }) + ] + }), + rgw_user_id: new FormControl('', { + validators: [ + CdValidators.requiredIf({ + name: 'RGW' + }) + ] + }) + }), + path: new FormControl(''), + protocolNfsv3: new FormControl(true, { + validators: [ + CdValidators.requiredIf({ protocolNfsv4: false }, (value) => { + return !value; + }) + ] + }), + protocolNfsv4: new FormControl(true, { + validators: [ + CdValidators.requiredIf({ protocolNfsv3: false }, (value) => { + return !value; + }) + ] + }), + tag: new FormControl(''), + pseudo: new FormControl('', { + validators: [Validators.required, Validators.pattern('^/[^><|&()]*$')] + }), + access_type: new FormControl('RW', { + validators: [Validators.required] + }), + squash: new FormControl('None', { + validators: [Validators.required] + }), + transportUDP: new FormControl(true, { + validators: [ + CdValidators.requiredIf({ transportTCP: false }, (value) => { + return !value; + }) + ] + }), + transportTCP: new FormControl(true, { + validators: [ + CdValidators.requiredIf({ transportUDP: false }, (value) => { + return !value; + }) + ] + }), + clients: this.formBuilder.array([]), + security_label: new FormControl(false), + sec_label_xattr: new FormControl( + 'security.selinux', + CdValidators.requiredIf({ security_label: true, 'fsal.name': 'CEPH' }) + ) + }); + } + + resolveModel(res) { + if (res.fsal.name === 'CEPH') { + res.sec_label_xattr = res.fsal.sec_label_xattr; + } + + this.daemonsSelections = _.map( + this.allDaemons[res.cluster_id], + (daemon) => new SelectOption(res.daemons.indexOf(daemon) !== -1, daemon, '') + ); + this.daemonsSelections = [...this.daemonsSelections]; + + res.protocolNfsv3 = res.protocols.indexOf(3) !== -1; + res.protocolNfsv4 = res.protocols.indexOf(4) !== -1; + delete res.protocols; + + res.transportTCP = res.transports.indexOf('TCP') !== -1; + res.transportUDP = res.transports.indexOf('UDP') !== -1; + delete res.transports; + + res.clients.forEach((client) => { + let addressStr = ''; + client.addresses.forEach((address) => { + addressStr += address + ', '; + }); + if (addressStr.length >= 2) { + addressStr = addressStr.substring(0, addressStr.length - 2); + } + client.addresses = addressStr; + }); + + this.nfsForm.patchValue(res); + this.setPathValidation(); + this.nfsClients.resolveModel(res.clients); + } + + resolveDaemons(daemons) { + daemons = _.sortBy(daemons, ['daemon_id']); + + this.allClusters = _(daemons) + .map((daemon) => daemon.cluster_id) + .sortedUniq() + .value(); + + _.forEach(this.allClusters, (cluster) => { + this.allDaemons[cluster] = []; + }); + + _.forEach(daemons, (daemon) => { + this.allDaemons[daemon.cluster_id].push(daemon.daemon_id); + }); + + const hasOneCluster = _.isArray(this.allClusters) && this.allClusters.length === 1; + this.isDefaultCluster = hasOneCluster && this.allClusters[0] === '_default_'; + if (hasOneCluster) { + this.nfsForm.patchValue({ + cluster_id: this.allClusters[0] + }); + this.onClusterChange(); + } + } + + resolvefsals(res: string[]) { + res.forEach((fsal) => { + const fsalItem = this.nfsService.nfsFsal.find((currentFsalItem) => { + return fsal === currentFsalItem.value; + }); + + if (_.isObjectLike(fsalItem)) { + this.allFsals.push(fsalItem); + if (fsalItem.value === 'RGW') { + this.rgwUserService.list().subscribe((result: any) => { + result.forEach((user) => { + if (user.suspended === 0 && user.keys.length > 0) { + this.allRgwUsers.push(user.user_id); + } + }); + }); + } + } + }); + + if (this.allFsals.length === 1 && _.isUndefined(this.nfsForm.getValue('fsal'))) { + this.nfsForm.patchValue({ + fsal: this.allFsals[0] + }); + } + } + + resolveClients(clients) { + this.allCephxClients = clients; + } + + resolveFilesystems(filesystems) { + this.allFsNames = filesystems; + if (filesystems.length === 1) { + this.nfsForm.patchValue({ + fsal: { + fs_name: filesystems[0].name + } + }); + } + } + + fsalChangeHandler() { + this.nfsForm.patchValue({ + tag: this._generateTag(), + pseudo: this._generatePseudo() + }); + + this.setPathValidation(); + + this.cdRef.detectChanges(); + } + + setPathValidation() { + if (this.nfsForm.getValue('name') === 'RGW') { + this.nfsForm + .get('path') + .setValidators([Validators.required, Validators.pattern('^(/|[^/><|&()#?]+)$')]); + } else { + this.nfsForm + .get('path') + .setValidators([Validators.required, Validators.pattern('^/[^><|&()?]*$')]); + } + } + + rgwUserIdChangeHandler() { + this.nfsForm.patchValue({ + pseudo: this._generatePseudo() + }); + } + + getAccessTypeHelp(accessType) { + const accessTypeItem = this.nfsAccessType.find((currentAccessTypeItem) => { + if (accessType === currentAccessTypeItem.value) { + return currentAccessTypeItem; + } + }); + return _.isObjectLike(accessTypeItem) ? accessTypeItem.help : ''; + } + + getId() { + if ( + _.isString(this.nfsForm.getValue('cluster_id')) && + _.isString(this.nfsForm.getValue('path')) + ) { + return this.nfsForm.getValue('cluster_id') + ':' + this.nfsForm.getValue('path'); + } + return ''; + } + + getPathTypeahead(path) { + if (!_.isString(path) || path === '/') { + return of([]); + } + + return this.nfsService.lsDir(path); + } + + pathChangeHandler() { + this.nfsForm.patchValue({ + pseudo: this._generatePseudo() + }); + + const path = this.nfsForm.getValue('path'); + this.getPathTypeahead(path).subscribe((res: any) => { + this.isNewDirectory = path !== '/' && res.paths.indexOf(path) === -1; + }); + } + + bucketChangeHandler() { + this.nfsForm.patchValue({ + tag: this._generateTag(), + pseudo: this._generatePseudo() + }); + + const bucket = this.nfsForm.getValue('path'); + this.getBucketTypeahead(bucket).subscribe((res: any) => { + this.isNewBucket = bucket !== '' && res.indexOf(bucket) === -1; + }); + } + + getBucketTypeahead(path: string): Observable { + const rgwUserId = this.nfsForm.getValue('rgw_user_id'); + + if (_.isString(rgwUserId) && _.isString(path) && path !== '/' && path !== '') { + return this.nfsService.buckets(rgwUserId); + } else { + return of([]); + } + } + + _generateTag() { + let newTag = this.nfsForm.getValue('tag'); + if (!this.nfsForm.get('tag').dirty) { + newTag = undefined; + if (this.nfsForm.getValue('fsal') === 'RGW') { + newTag = this.nfsForm.getValue('path'); + } + } + return newTag; + } + + _generatePseudo() { + let newPseudo = this.nfsForm.getValue('pseudo'); + if (this.nfsForm.get('pseudo') && !this.nfsForm.get('pseudo').dirty) { + newPseudo = undefined; + if (this.nfsForm.getValue('fsal') === 'CEPH') { + newPseudo = '/cephfs'; + if (_.isString(this.nfsForm.getValue('path'))) { + newPseudo += this.nfsForm.getValue('path'); + } + } else if (this.nfsForm.getValue('fsal') === 'RGW') { + if (_.isString(this.nfsForm.getValue('rgw_user_id'))) { + newPseudo = '/' + this.nfsForm.getValue('rgw_user_id'); + if (_.isString(this.nfsForm.getValue('path'))) { + newPseudo += '/' + this.nfsForm.getValue('path'); + } + } + } + } + return newPseudo; + } + + onClusterChange() { + const cluster_id = this.nfsForm.getValue('cluster_id'); + this.daemonsSelections = _.map( + this.allDaemons[cluster_id], + (daemon) => new SelectOption(false, daemon, '') + ); + this.daemonsSelections = [...this.daemonsSelections]; + this.nfsForm.patchValue({ daemons: [] }); + } + + removeDaemon(index, daemon) { + this.daemonsSelections.forEach((value) => { + if (value.name === daemon) { + value.selected = false; + } + }); + + const daemons = this.nfsForm.get('daemons'); + daemons.value.splice(index, 1); + daemons.setValue(daemons.value); + + return false; + } + + onDaemonSelection() { + this.nfsForm.get('daemons').setValue(this.nfsForm.getValue('daemons')); + } + + submitAction() { + let action: Observable; + const requestModel = this._buildRequest(); + + if (this.isEdit) { + action = this.taskWrapper.wrapTaskAroundCall({ + task: new FinishedTask('nfs/edit', { + cluster_id: this.cluster_id, + export_id: this.export_id + }), + call: this.nfsService.update(this.cluster_id, this.export_id, requestModel) + }); + } else { + // Create + action = this.taskWrapper.wrapTaskAroundCall({ + task: new FinishedTask('nfs/create', { + path: requestModel.path, + fsal: requestModel.fsal, + cluster_id: requestModel.cluster_id + }), + call: this.nfsService.create(requestModel) + }); + } + + action.subscribe( + undefined, + () => this.nfsForm.setErrors({ cdSubmitButton: true }), + () => this.router.navigate(['/nfs']) + ); + } + + _buildRequest() { + const requestModel: any = _.cloneDeep(this.nfsForm.value); + + if (_.isUndefined(requestModel.tag) || requestModel.tag === '') { + requestModel.tag = null; + } + + if (this.isEdit) { + requestModel.export_id = this.export_id; + } + + if (requestModel.fsal.name === 'CEPH') { + delete requestModel.fsal.rgw_user_id; + } else { + delete requestModel.fsal.fs_name; + delete requestModel.fsal.user_id; + } + + requestModel.protocols = []; + if (requestModel.protocolNfsv3) { + delete requestModel.protocolNfsv3; + requestModel.protocols.push(3); + } else { + requestModel.tag = null; + } + if (requestModel.protocolNfsv4) { + delete requestModel.protocolNfsv4; + requestModel.protocols.push(4); + } else { + requestModel.pseudo = null; + } + + requestModel.transports = []; + if (requestModel.transportTCP) { + delete requestModel.transportTCP; + requestModel.transports.push('TCP'); + } + if (requestModel.transportUDP) { + delete requestModel.transportUDP; + requestModel.transports.push('UDP'); + } + + requestModel.clients.forEach((client) => { + if (_.isString(client.addresses)) { + client.addresses = _(client.addresses) + .split(/[ ,]+/) + .uniq() + .filter((address) => address !== '') + .value(); + } else { + client.addresses = []; + } + }); + + if (requestModel.security_label === false || requestModel.fsal.name === 'RGW') { + requestModel.fsal.sec_label_xattr = null; + } else { + requestModel.fsal.sec_label_xattr = requestModel.sec_label_xattr; + } + delete requestModel.sec_label_xattr; + + return requestModel; + } + + cancelAction() { + this.router.navigate(['/nfs']); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ordinal.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ordinal.pipe.spec.ts new file mode 100644 index 0000000000000..7e1cdbc8d3d86 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ordinal.pipe.spec.ts @@ -0,0 +1,8 @@ +import { OrdinalPipe } from './ordinal.pipe'; + +describe('OrdinalPipe', () => { + it('create an instance', () => { + const pipe = new OrdinalPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ordinal.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ordinal.pipe.ts new file mode 100644 index 0000000000000..d2e176cf6bd3b --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ordinal.pipe.ts @@ -0,0 +1,25 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'ordinal' +}) +export class OrdinalPipe implements PipeTransform { + transform(value: any): any { + const num = parseInt(value, 10); + if (isNaN(num)) { + return value; + } + return ( + value + + (Math.floor(num / 10) === 1 + ? 'th' + : num % 10 === 1 + ? 'st' + : num % 10 === 2 + ? 'nd' + : num % 10 === 3 + ? 'rd' + : 'th') + ); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts index cb5b69c0a205b..c7870011592cd 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts @@ -12,6 +12,7 @@ import { FilterPipe } from './filter.pipe'; import { HealthColorPipe } from './health-color.pipe'; import { ListPipe } from './list.pipe'; import { LogPriorityPipe } from './log-priority.pipe'; +import { OrdinalPipe } from './ordinal.pipe'; import { RelativeDatePipe } from './relative-date.pipe'; import { RoundPipe } from './round.pipe'; @@ -30,7 +31,8 @@ import { RoundPipe } from './round.pipe'; CdDatePipe, EmptyPipe, EncodeUriPipe, - RoundPipe + RoundPipe, + OrdinalPipe ], exports: [ DimlessBinaryPipe, @@ -45,7 +47,8 @@ import { RoundPipe } from './round.pipe'; CdDatePipe, EmptyPipe, EncodeUriPipe, - RoundPipe + RoundPipe, + OrdinalPipe ], providers: [ DatePipe, @@ -58,7 +61,8 @@ import { RoundPipe } from './round.pipe'; LogPriorityPipe, CdDatePipe, EmptyPipe, - EncodeUriPipe + EncodeUriPipe, + OrdinalPipe ] }) export class PipesModule {} diff --git a/src/pybind/mgr/dashboard/frontend/src/locale/messages.xlf b/src/pybind/mgr/dashboard/frontend/src/locale/messages.xlf index 9e2791db8a7a7..f53416f6e7ffa 100644 --- a/src/pybind/mgr/dashboard/frontend/src/locale/messages.xlf +++ b/src/pybind/mgr/dashboard/frontend/src/locale/messages.xlf @@ -20,6 +20,10 @@ app/core/navigation/navigation/navigation.component.html 47 + + app/ceph/nfs/nfs-form/nfs-form.component.html + 21 + Hosts @@ -146,23 +150,33 @@ app/core/navigation/navigation/navigation.component.html 158 + + NFS + + app/core/navigation/navigation/navigation.component.html + 168 + Filesystems app/core/navigation/navigation/navigation.component.html - 169 + 176 Object Gateway app/core/navigation/navigation/navigation.component.html - 180 + 187 + + + app/ceph/nfs/nfs-list/nfs-list.component.html + 32 Daemons app/core/navigation/navigation/navigation.component.html - 189 + 196 app/ceph/block/iscsi/iscsi.component.html @@ -172,11 +186,15 @@ app/ceph/block/mirroring/overview/overview.component.html 5 + + app/ceph/nfs/nfs-form/nfs-form.component.html + 53 + Users app/core/navigation/navigation/navigation.component.html - 195 + 202 app/core/auth/user-tabs/user-tabs.component.html @@ -186,7 +204,7 @@ Buckets app/core/navigation/navigation/navigation.component.html - 201 + 208 Retrieving data for @@ -228,6 +246,10 @@ app/ceph/cluster/configuration/configuration-form/configuration-form.component.html 159 + + app/ceph/nfs/nfs-form/nfs-form.component.html + 520 + app/ceph/pool/pool-form/pool-form.component.html 443 @@ -892,6 +914,10 @@ app/ceph/cluster/osd/osd-scrub-modal/osd-scrub-modal.component.html 21 + + app/ceph/nfs/nfs-form/nfs-form.component.html + 515 + app/ceph/block/mirroring/pool-edit-peer-modal/pool-edit-peer-modal.component.html 106 @@ -986,6 +1012,34 @@ app/ceph/block/rbd-form/rbd-form.component.html 139 + + app/ceph/nfs/nfs-form/nfs-form.component.html + 32 + + + app/ceph/nfs/nfs-form/nfs-form.component.html + 106 + + + app/ceph/nfs/nfs-form/nfs-form.component.html + 139 + + + app/ceph/nfs/nfs-form/nfs-form.component.html + 171 + + + app/ceph/nfs/nfs-form/nfs-form.component.html + 204 + + + app/ceph/nfs/nfs-form/nfs-form.component.html + 418 + + + app/ceph/nfs/nfs-form/nfs-form.component.html + 453 + app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form.component.html 59 @@ -1050,6 +1104,10 @@ app/ceph/block/rbd-form/rbd-form.component.html 142 + + app/ceph/nfs/nfs-form/nfs-form.component.html + 109 + Size @@ -1573,6 +1631,10 @@ app/ceph/cephfs/cephfs-detail/cephfs-detail.component.html 3 + + app/ceph/nfs/nfs-details/nfs-details.component.html + 2 + Health @@ -1585,6 +1647,381 @@ app/ceph/dashboard/dashboard/dashboard.component.html 8 + + Clients + + app/ceph/nfs/nfs-form-client/nfs-form-client.component.html + 3 + + + Any client can access + + app/ceph/nfs/nfs-form-client/nfs-form-client.component.html + 11 + + + Addresses + + app/ceph/nfs/nfs-form-client/nfs-form-client.component.html + 32 + + + Required field + + app/ceph/nfs/nfs-form-client/nfs-form-client.component.html + 42 + + + app/ceph/nfs/nfs-form/nfs-form.component.html + 44 + + + app/ceph/nfs/nfs-form/nfs-form.component.html + 118 + + + app/ceph/nfs/nfs-form/nfs-form.component.html + 151 + + + app/ceph/nfs/nfs-form/nfs-form.component.html + 183 + + + app/ceph/nfs/nfs-form/nfs-form.component.html + 216 + + + app/ceph/nfs/nfs-form/nfs-form.component.html + 253 + + + app/ceph/nfs/nfs-form/nfs-form.component.html + 277 + + + app/ceph/nfs/nfs-form/nfs-form.component.html + 309 + + + app/ceph/nfs/nfs-form/nfs-form.component.html + 349 + + + app/ceph/nfs/nfs-form/nfs-form.component.html + 396 + + + app/ceph/nfs/nfs-form/nfs-form.component.html + 434 + + + app/ceph/nfs/nfs-form/nfs-form.component.html + 466 + + + app/ceph/nfs/nfs-form/nfs-form.component.html + 498 + + + Must contain one or more comma-separated values + + app/ceph/nfs/nfs-form-client/nfs-form-client.component.html + 45 + + + For example: + + app/ceph/nfs/nfs-form-client/nfs-form-client.component.html + 47 + + + Access Type + + app/ceph/nfs/nfs-form-client/nfs-form-client.component.html + 57 + + + app/ceph/nfs/nfs-form/nfs-form.component.html + 408 + + + Squash + + app/ceph/nfs/nfs-form-client/nfs-form-client.component.html + 78 + + + app/ceph/nfs/nfs-form/nfs-form.component.html + 443 + + + Add clients + + app/ceph/nfs/nfs-form-client/nfs-form-client.component.html + 99 + + + NFS export + + app/ceph/nfs/nfs-form/nfs-form.component.html + 10 + + + -- No cluster available -- + + app/ceph/nfs/nfs-form/nfs-form.component.html + 35 + + + -- Select the cluster -- + + app/ceph/nfs/nfs-form/nfs-form.component.html + 38 + + + Add daemon + + app/ceph/nfs/nfs-form/nfs-form.component.html + 81 + + + Storage Backend + + app/ceph/nfs/nfs-form/nfs-form.component.html + 95 + + + -- Select the storage backend -- + + app/ceph/nfs/nfs-form/nfs-form.component.html + 112 + + + Object Gateway User + + app/ceph/nfs/nfs-form/nfs-form.component.html + 128 + + + -- No users available -- + + app/ceph/nfs/nfs-form/nfs-form.component.html + 142 + + + app/ceph/nfs/nfs-form/nfs-form.component.html + 207 + + + -- Select the object gateway user -- + + app/ceph/nfs/nfs-form/nfs-form.component.html + 145 + + + app/ceph/nfs/nfs-form/nfs-form.component.html + 210 + + + CephFS User ID + + app/ceph/nfs/nfs-form/nfs-form.component.html + 161 + + + -- No clients available -- + + app/ceph/nfs/nfs-form/nfs-form.component.html + 174 + + + -- Select the cephx client -- + + app/ceph/nfs/nfs-form/nfs-form.component.html + 177 + + + CephFS Name + + app/ceph/nfs/nfs-form/nfs-form.component.html + 193 + + + Security Label + + app/ceph/nfs/nfs-form/nfs-form.component.html + 227 + + + Enable security label + + app/ceph/nfs/nfs-form/nfs-form.component.html + 239 + + + CephFS Path + + app/ceph/nfs/nfs-form/nfs-form.component.html + 263 + + + Path need to start with a '/' and can be followed by a word + + app/ceph/nfs/nfs-form/nfs-form.component.html + 281 + + + New directory will be created + + app/ceph/nfs/nfs-form/nfs-form.component.html + 284 + + + Path + + app/ceph/nfs/nfs-form/nfs-form.component.html + 294 + + + Path can only be a single '/' or a word + + app/ceph/nfs/nfs-form/nfs-form.component.html + 313 + + + New bucket will be created + + app/ceph/nfs/nfs-form/nfs-form.component.html + 317 + + + NFS Protocol + + app/ceph/nfs/nfs-form/nfs-form.component.html + 326 + + + NFSv3 + + app/ceph/nfs/nfs-form/nfs-form.component.html + 336 + + + NFSv4 + + app/ceph/nfs/nfs-form/nfs-form.component.html + 344 + + + NFS Tag + + app/ceph/nfs/nfs-form/nfs-form.component.html + 358 + + + Alternative access for NFS v3 mounts (it must not have a leading /). + + app/ceph/nfs/nfs-form/nfs-form.component.html + 360 + + + Clients may not mount subdirectories (i.e. if Tag = foo, the client may not mount foo/baz). + + app/ceph/nfs/nfs-form/nfs-form.component.html + 361 + + + By using different Tag options, the same Path may be exported multiple times. + + app/ceph/nfs/nfs-form/nfs-form.component.html + 362 + + + Pseudo + + app/ceph/nfs/nfs-form/nfs-form.component.html + 380 + + + The position that this NFS v4 export occupies + in the Pseudo FS (it must be unique). + + app/ceph/nfs/nfs-form/nfs-form.component.html + 383 + + + By using different Pseudo options, the same Path may be exported multiple times. + + app/ceph/nfs/nfs-form/nfs-form.component.html + 385 + + + Wrong format + + app/ceph/nfs/nfs-form/nfs-form.component.html + 399 + + + -- No access type available -- + + app/ceph/nfs/nfs-form/nfs-form.component.html + 421 + + + -- Select the access type -- + + app/ceph/nfs/nfs-form/nfs-form.component.html + 424 + + + -- No squash available -- + + app/ceph/nfs/nfs-form/nfs-form.component.html + 456 + + + --Select what kind of user id squashing is performed -- + + app/ceph/nfs/nfs-form/nfs-form.component.html + 459 + + + Transport Protocol + + app/ceph/nfs/nfs-form/nfs-form.component.html + 475 + + + UDP + + app/ceph/nfs/nfs-form/nfs-form.component.html + 485 + + + TCP + + app/ceph/nfs/nfs-form/nfs-form.component.html + 493 + + + To apply any changes you have to restart the Ganisha services. + + app/ceph/nfs/nfs-list/nfs-list.component.html + 4 + + + Orchestrator not available + + app/ceph/nfs/nfs-list/nfs-list.component.html + 2 + + + CephFS + + app/ceph/nfs/nfs-list/nfs-list.component.html + 30 + Add erasure code profile @@ -4926,6 +5363,62 @@ 1 + + Transport + + src/app/ceph/nfs/nfs-details/nfs-details.component.ts + 1 + + + + CephFS User + + src/app/ceph/nfs/nfs-details/nfs-details.component.ts + 1 + + + + CephFS Filesystem + + src/app/ceph/nfs/nfs-details/nfs-details.component.ts + 1 + + + + (inherited from global config) + + src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.ts + 1 + + + + inherited from global config + + src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.ts + 1 + + + + -- Select what kind of user id squashing is performed -- + + src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.ts + 1 + + + + There are no daemons available. + + src/app/ceph/nfs/nfs-form/nfs-form.component.ts + 1 + + + + Export + + src/app/ceph/nfs/nfs-list/nfs-list.component.ts + 1 + + Value @@ -5350,6 +5843,41 @@ 1 + + Allows all operations + + src/app/shared/api/nfs.service.ts + 1 + + + + Allows only operations that do not modify the server + + src/app/shared/api/nfs.service.ts + 1 + + + + Does not allow read or write operations, but allows any other operation + + src/app/shared/api/nfs.service.ts + 1 + + + + Does not allow read, write, or any operation that modifies file attributes or directory content + + src/app/shared/api/nfs.service.ts + 1 + + + + Allows no access at all + + src/app/shared/api/nfs.service.ts + 1 + + -- Select the priority -- @@ -6065,6 +6593,13 @@ 1 + + NFS + + src/app/shared/services/task-message.service.ts + 1 + + -- 2.39.5