]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Add UI for NFS Form 26085/head
authorTiago Melo <tspmelo@gmail.com>
Thu, 4 Oct 2018 16:24:20 +0000 (17:24 +0100)
committerTiago Melo <tmelo@suse.com>
Thu, 14 Feb 2019 10:34:31 +0000 (10:34 +0000)
Signed-off-by: Tiago Melo <tmelo@suse.com>
12 files changed:
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ordinal.pipe.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/ordinal.pipe.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts
src/pybind/mgr/dashboard/frontend/src/locale/messages.xlf

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 (file)
index 0000000..fa75da4
--- /dev/null
@@ -0,0 +1,104 @@
+<div class="form-group">
+  <label class="col-sm-3 control-label"
+         i18n>Clients</label>
+
+  <div class="col-sm-9"
+       [formGroup]="form"
+       #formDir="ngForm">
+    <span *ngIf="form.get('clients').value.length === 0"
+          class="form-control no-border text-muted">
+      <span class="text-muted"
+            i18n>Any client can access</span>
+    </span>
+
+    <ng-container formArrayName="clients">
+      <div *ngFor="let item of form.get('clients').value; let index = index; trackBy: trackByFn">
+        <div class="panel panel-default"
+             [formGroupName]="index">
+          <div class="panel-heading">
+            <h3 class="panel-title">{{ (index + 1) | ordinal }}
+              <span class="pull-right clickable"
+                    (click)="removeClient(index)"
+                    tooltip="Remove">&times;</span>
+            </h3>
+          </div>
+
+          <div class="panel-body">
+            <!-- Addresses -->
+            <div class="form-group"
+                 [ngClass]="{ 'has-error': showError(index, 'addresses', formDir) }">
+              <label i18n
+                     class="col-sm-3 control-label"
+                     for="addresses">Addresses</label>
+              <div class="col-sm-9">
+                <input type="text"
+                       class="form-control"
+                       name="addresses"
+                       id="addresses"
+                       formControlName="addresses"
+                       placeholder="192.168.0.10, 192.168.1.0/8">
+                <span class="help-block">
+                  <span *ngIf="showError(index, 'addresses', formDir, 'required')"
+                        i18n>Required field</span>
+
+                  <span *ngIf="showError(index, 'addresses', formDir, 'pattern')">
+                    <ng-container i18n>Must contain one or more comma-separated values</ng-container>
+                    <br>
+                    <ng-container i18n>For example:</ng-container> 192.168.0.10, 192.168.1.0/8
+                  </span>
+                </span>
+              </div>
+            </div>
+
+            <!-- Access Type-->
+            <div class="form-group">
+              <label i18n
+                     class="col-sm-3 control-label"
+                     for="access_type">Access Type</label>
+              <div class="col-sm-9">
+                <select class="form-control"
+                        name="access_type"
+                        id="access_type"
+                        formControlName="access_type">
+                  <option [value]="form.getValue('access_type')">{{ getNoAccessTypeDescr() }}</option>
+                  <option *ngFor="let item of nfsAccessType"
+                          [value]="item.value">{{ item.value }}</option>
+                </select>
+                <span class="help-block"
+                      *ngIf="getValue(index, 'access_type')">
+                  {{ getAccessTypeHelp(index) }}
+                </span>
+              </div>
+            </div>
+
+            <!-- Squash -->
+            <div class="form-group">
+              <label i18n
+                     class="col-sm-3 control-label"
+                     for="squash">Squash</label>
+              <div class="col-sm-9">
+                <select class="form-control"
+                        name="squash"
+                        id="squash"
+                        formControlName="squash">
+                  <option [value]="form.getValue('squash')">{{ getNoSquashDescr() }}</option>
+                  <option *ngFor="let squash of nfsSquash"
+                          [value]="squash">{{ squash }}</option>
+                </select>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </ng-container>
+
+    <span class="form-control no-border">
+      <button class="btn btn-default btn-label pull-right"
+              (click)="addClient()">
+        <i class="fa fa-fw fa-plus"></i>
+        <ng-container i18n>Add clients</ng-container>
+      </button>
+    </span>
+    <hr>
+  </div>
+</div>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..3c8e0bb
--- /dev/null
@@ -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<NfsFormClientComponent>;
+
+  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 (file)
index 0000000..986dd5a
--- /dev/null
@@ -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 (<any>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 (file)
index 0000000..d67ddd7
--- /dev/null
@@ -0,0 +1,525 @@
+<div class="col-sm-12 col-lg-6">
+  <form name="nfsForm"
+        class="form-horizontal"
+        #formDir="ngForm"
+        [formGroup]="nfsForm"
+        novalidate>
+    <div class="panel panel-default">
+      <div class="panel-heading">
+        <h3 class="panel-title"
+            i18n>NFS export {{ export_id ? cluster_id + ':' + export_id : '' }}</h3>
+      </div>
+
+      <div class="panel-body">
+
+        <!-- cluster_id -->
+        <div class="form-group"
+             [ngClass]="{'has-error': nfsForm.showError('cluster_id', formDir)}"
+             *ngIf="!isDefaultCluster">
+          <label class="col-sm-3 control-label"
+                 for="cluster_id">
+            <ng-container i18n>Cluster</ng-container>
+            <span class="required"></span>
+          </label>
+          <div class="col-sm-9">
+            <select class="form-control"
+                    formControlName="cluster_id"
+                    name="cluster_id"
+                    id="cluster_id"
+                    (change)="onClusterChange()">
+              <option *ngIf="allClusters === null"
+                      value=""
+                      i18n>Loading...</option>
+              <option *ngIf="allClusters !== null && allClusters.length === 0"
+                      value=""
+                      i18n>-- No cluster available --</option>
+              <option *ngIf="allClusters !== null && allClusters.length > 0"
+                      value=""
+                      i18n>-- Select the cluster --</option>
+              <option *ngFor="let cluster of allClusters"
+                      [value]="cluster">{{ cluster }}</option>
+            </select>
+            <span class="help-block"
+                  *ngIf="nfsForm.showError('cluster_id', formDir, 'required')"
+                  i18n>Required field</span>
+          </div>
+        </div>
+
+        <!-- daemons -->
+        <div class="form-group"
+             [ngClass]="{'has-error': nfsForm.showError('daemons', formDir)}">
+          <label class="col-sm-3 control-label"
+                 for="daemons">
+            <ng-container i18n>Daemons</ng-container>
+          </label>
+          <div class="col-sm-9">
+            <ng-container *ngFor="let daemon of nfsForm.getValue('daemons'); let i = index">
+              <div class="input-group cd-mb">
+                <input class="form-control"
+                       type="text"
+                       [value]="daemon"
+                       disabled />
+                <span class="input-group-btn">
+                  <button class="btn btn-default"
+                          type="button"
+                          (click)="removeDaemon(i, daemon)">
+                    <i class="fa fa-remove fa-fw"
+                       aria-hidden="true"></i>
+                  </button>
+                </span>
+              </div>
+            </ng-container>
+
+            <div class="row">
+              <div class="col-md-12">
+                <cd-select [data]="nfsForm.get('daemons').value"
+                           [options]="daemonsSelections"
+                           [messages]="daemonsMessages"
+                           (selection)="onDaemonSelection()"
+                           elemClass="btn btn-default pull-right">
+                  <i class="fa fa-fw fa-plus"></i>
+                  <ng-container i18n>Add daemon</ng-container>
+                </cd-select>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <!-- FSAL -->
+        <div formGroupName="fsal">
+          <!-- Name -->
+          <div class="form-group"
+               [ngClass]="{'has-error': nfsForm.showError('name', formDir)}">
+            <label class="col-sm-3 control-label"
+                   for="name">
+              <ng-container i18n>Storage Backend</ng-container>
+              <span class="required"></span>
+            </label>
+            <div class="col-sm-9">
+              <select class="form-control"
+                      formControlName="name"
+                      name="name"
+                      id="name"
+                      (change)="fsalChangeHandler()">
+                <option *ngIf="allFsals === null"
+                        value=""
+                        i18n>Loading...</option>
+                <option *ngIf="allFsals !== null && allFsals.length === 0"
+                        value=""
+                        i18n>-- No data pools available --</option>
+                <option *ngIf="allFsals !== null && allFsals.length > 0"
+                        value=""
+                        i18n>-- Select the storage backend --</option>
+                <option *ngFor="let fsal of allFsals"
+                        [value]="fsal.value">{{ fsal.descr }}</option>
+              </select>
+              <span class="help-block"
+                    *ngIf="nfsForm.showError('name', formDir, 'required')"
+                    i18n>Required field</span>
+            </div>
+          </div>
+
+          <!-- RGW user -->
+          <div class="form-group"
+               [ngClass]="{'has-error': nfsForm.showError('rgw_user_id', formDir)}"
+               *ngIf="nfsForm.getValue('name') === 'RGW'">
+            <label class="col-sm-3 control-label"
+                   for="rgw_user_id">
+              <ng-container i18n>Object Gateway User</ng-container>
+              <span class="required"></span>
+            </label>
+            <div class="col-sm-9">
+              <select class="form-control"
+                      formControlName="rgw_user_id"
+                      name="rgw_user_id"
+                      id="rgw_user_id"
+                      (change)="rgwUserIdChangeHandler()">
+                <option *ngIf="allRgwUsers === null"
+                        value=""
+                        i18n>Loading...</option>
+                <option *ngIf="allRgwUsers !== null && allRgwUsers.length === 0"
+                        value=""
+                        i18n>-- No users available --</option>
+                <option *ngIf="allRgwUsers !== null && allRgwUsers.length > 0"
+                        value=""
+                        i18n>-- Select the object gateway user --</option>
+                <option *ngFor="let rgwUserId of allRgwUsers"
+                        [value]="rgwUserId">{{ rgwUserId }}</option>
+              </select>
+              <span class="help-block"
+                    *ngIf="nfsForm.showError('rgw_user_id', formDir, 'required')"
+                    i18n>Required field</span>
+            </div>
+          </div>
+
+          <!-- CephFS user_id -->
+          <div class="form-group"
+               [ngClass]="{'has-error': nfsForm.showError('user_id', formDir)}"
+               *ngIf="nfsForm.getValue('name') === 'CEPH'">
+            <label class="col-sm-3 control-label"
+                   for="user_id">
+              <ng-container i18n>CephFS User ID</ng-container>
+              <span class="required"></span>
+            </label>
+            <div class="col-sm-9">
+              <select class="form-control"
+                      formControlName="user_id"
+                      name="user_id"
+                      id="user_id">
+                <option *ngIf="allCephxClients === null"
+                        value=""
+                        i18n>Loading...</option>
+                <option *ngIf="allCephxClients !== null && allCephxClients.length === 0"
+                        value=""
+                        i18n>-- No clients available --</option>
+                <option *ngIf="allCephxClients !== null && allCephxClients.length > 0"
+                        value=""
+                        i18n>-- Select the cephx client --</option>
+                <option *ngFor="let client of allCephxClients"
+                        [value]="client">{{ client }}</option>
+              </select>
+              <span class="help-block"
+                    *ngIf="nfsForm.showError('user_id', formDir, 'required')"
+                    i18n>Required field</span>
+            </div>
+          </div>
+
+          <!-- CephFS fs_name -->
+          <div class="form-group"
+               [ngClass]="{'has-error': nfsForm.showError('fs_name', formDir)}"
+               *ngIf="nfsForm.getValue('name') === 'CEPH'">
+            <label class="col-sm-3 control-label"
+                   for="fs_name">
+              <ng-container i18n>CephFS Name</ng-container>
+              <span class="required"></span>
+            </label>
+            <div class="col-sm-9">
+              <select class="form-control"
+                      formControlName="fs_name"
+                      name="fs_name"
+                      id="fs_name"
+                      (change)="rgwUserIdChangeHandler()">
+                <option *ngIf="allFsNames === null"
+                        value=""
+                        i18n>Loading...</option>
+                <option *ngIf="allFsNames !== null && allFsNames.length === 0"
+                        value=""
+                        i18n>-- No users available --</option>
+                <option *ngIf="allFsNames !== null && allFsNames.length > 0"
+                        value=""
+                        i18n>-- Select the object gateway user --</option>
+                <option *ngFor="let filesystem of allFsNames"
+                        [value]="filesystem.name">{{ filesystem.name }}</option>
+              </select>
+              <span class="help-block"
+                    *ngIf="nfsForm.showError('fs_name', formDir, 'required')"
+                    i18n>Required field</span>
+            </div>
+          </div>
+        </div>
+
+        <!-- Secutiry Label -->
+        <div class="form-group"
+             [ngClass]="{'has-error': nfsForm.showError('security_label', formDir)}"
+             *ngIf="nfsForm.getValue('name') === 'CEPH'">
+          <label class="col-sm-3 control-label"
+                 for="security_label">
+            <ng-container i18n>Security Label</ng-container>
+            <span class="required"
+                  *ngIf="nfsForm.getValue('security_label')"></span>
+          </label>
+
+          <div class="col-sm-9">
+            <div class="checkbox checkbox-primary">
+              <input type="checkbox"
+                     formControlName="security_label"
+                     name="security_label"
+                     id="security_label">
+              <label for="security_label"
+                     i18n>Enable security label</label>
+            </div>
+
+            <br>
+
+            <input type="text"
+                   *ngIf="nfsForm.getValue('security_label')"
+                   class="form-control"
+                   name="sec_label_xattr"
+                   id="sec_label_xattr"
+                   formControlName="sec_label_xattr">
+
+            <span class="help-block"
+                  *ngIf="nfsForm.showError('sec_label_xattr', formDir, 'required')"
+                  i18n>Required field</span>
+          </div>
+        </div>
+
+        <!-- Path -->
+        <div class="form-group"
+             [ngClass]="{'has-error': nfsForm.showError('path', formDir)}"
+             *ngIf="nfsForm.getValue('name') === 'CEPH'">
+          <label class="col-sm-3 control-label"
+                 for="path">
+            <ng-container i18n>CephFS Path</ng-container>
+            <span class="required"></span>
+          </label>
+          <div class="col-sm-9">
+            <input type="text"
+                   class="form-control"
+                   name="path"
+                   id="path"
+                   formControlName="path"
+                   [typeahead]="pathDataSource"
+                   (typeaheadOnSelect)="pathChangeHandler()"
+                   (blur)="pathChangeHandler()">
+            <span class="help-block"
+                  *ngIf="nfsForm.showError('path', formDir, 'required')"
+                  i18n>Required field</span>
+
+            <span class="help-block"
+                  *ngIf="nfsForm.showError('path', formDir, 'pattern')"
+                  i18n>Path need to start with a '/' and can be followed by a word</span>
+            <span class="help-block"
+                  *ngIf="isNewDirectory && !nfsForm.showError('path', formDir)"
+                  i18n>New directory will be created</span>
+          </div>
+        </div>
+
+        <!-- Bucket -->
+        <div class="form-group"
+             [ngClass]="{'has-error': nfsForm.showError('path', formDir)}"
+             *ngIf="nfsForm.getValue('name') === 'RGW'">
+          <label class="col-sm-3 control-label"
+                 for="path">
+            <ng-container i18n>Path</ng-container>
+            <span class="required"></span>
+          </label>
+          <div class="col-sm-9">
+            <input type="text"
+                   class="form-control"
+                   name="path"
+                   id="path"
+                   formControlName="path"
+                   [typeahead]="bucketDataSource"
+                   (typeaheadOnSelect)="bucketChangeHandler()"
+                   (blur)="bucketChangeHandler()">
+
+            <span class="help-block"
+                  *ngIf="nfsForm.showError('path', formDir, 'required')"
+                  i18n>Required field</span>
+
+            <span class="help-block"
+                  *ngIf="nfsForm.showError('path', formDir, 'pattern')"
+                  i18n>Path can only be a single '/' or a word</span>
+
+            <span class="help-block"
+                  *ngIf="isNewBucket && !nfsForm.showError('path', formDir)"
+                  i18n>New bucket will be created</span>
+          </div>
+        </div>
+
+        <!-- NFS Protocol -->
+        <div class="form-group"
+             [ngClass]="{'has-error': nfsForm.showError('protocolNfsv3', formDir) || nfsForm.showError('protocolNfsv4', formDir)}">
+          <label class="col-sm-3 control-label"
+                 for="protocols">
+            <ng-container i18n>NFS Protocol</ng-container>
+            <span class="required"></span>
+          </label>
+          <div class="col-sm-9">
+            <div class="checkbox checkbox-primary">
+              <input type="checkbox"
+                     id="protocolNfsv3"
+                     name="protocolNfsv3"
+                     formControlName="protocolNfsv3">
+              <label i18n
+                     for="protocolNfsv3">NFSv3</label>
+            </div>
+            <div class="checkbox checkbox-primary">
+              <input type="checkbox"
+                     formControlName="protocolNfsv4"
+                     name="protocolNfsv4"
+                     id="protocolNfsv4">
+              <label i18n
+                     for="protocolNfsv4">NFSv4</label>
+            </div>
+            <span class="help-block"
+                  *ngIf="nfsForm.showError('protocolNfsv3', formDir, 'required') ||
+                  nfsForm.showError('protocolNfsv4', formDir, 'required')"
+                  i18n>Required field</span>
+          </div>
+        </div>
+
+        <!-- Tag -->
+        <div class="form-group"
+             *ngIf="nfsForm.getValue('protocolNfsv3')">
+          <label class="col-sm-3 control-label"
+                 for="tag">
+            <ng-container i18n>NFS Tag</ng-container>
+            <cd-helper>
+              <p i18n>Alternative access for <strong>NFS v3</strong> mounts (it must not have a leading /).</p>
+              <p i18n>Clients may not mount subdirectories (i.e. if Tag = foo, the client may not mount foo/baz).</p>
+              <p i18n>By using different Tag options, the same Path may be exported multiple times.</p>
+            </cd-helper>
+          </label>
+          <div class="col-sm-9">
+            <input type="text"
+                   class="form-control"
+                   name="tag"
+                   id="tag"
+                   formControlName="tag">
+          </div>
+        </div>
+
+        <!-- Pseudo -->
+        <div class="form-group"
+             [ngClass]="{'has-error': nfsForm.showError('pseudo', formDir)}"
+             *ngIf="nfsForm.getValue('protocolNfsv4')">
+          <label class="col-sm-3 control-label"
+                 for="pseudo">
+            <ng-container i18n>Pseudo</ng-container>
+            <span class="required"></span>
+            <cd-helper>
+              <p i18n>The position that this <strong>NFS v4</strong> export occupies
+                in the <strong>Pseudo FS</strong> (it must be unique).</p>
+              <p i18n>By using different Pseudo options, the same Path may be exported multiple times.</p>
+            </cd-helper>
+          </label>
+          <div class="col-sm-9">
+            <input type="text"
+                   class="form-control"
+                   name="pseudo"
+                   id="pseudo"
+                   formControlName="pseudo">
+            <span class="help-block"
+                  *ngIf="nfsForm.showError('pseudo', formDir, 'required')"
+                  i18n>Required field</span>
+            <span class="help-block"
+                  *ngIf="nfsForm.showError('pseudo', formDir, 'pattern')"
+                  i18n>Wrong format</span>
+          </div>
+        </div>
+
+        <!-- Access Type -->
+        <div class="form-group"
+             [ngClass]="{'has-error': nfsForm.showError('access_type', formDir)}">
+          <label class="col-sm-3 control-label"
+                 for="access_type">
+            <ng-container i18n>Access Type</ng-container>
+            <span class="required"></span>
+          </label>
+          <div class="col-sm-9">
+            <select class="form-control"
+                    formControlName="access_type"
+                    name="access_type"
+                    id="access_type">
+              <option *ngIf="nfsAccessType === null"
+                      value=""
+                      i18n>Loading...</option>
+              <option *ngIf="nfsAccessType !== null && nfsAccessType.length === 0"
+                      value=""
+                      i18n>-- No access type available --</option>
+              <option *ngIf="nfsAccessType !== null && nfsAccessType.length > 0"
+                      value=""
+                      i18n>-- Select the access type --</option>
+              <option *ngFor="let accessType of nfsAccessType"
+                      [value]="accessType.value">{{ accessType.value }}</option>
+            </select>
+            <span class="help-block"
+                  *ngIf="nfsForm.getValue('access_type')">
+              {{ getAccessTypeHelp(nfsForm.getValue('access_type')) }}
+            </span>
+            <span class="help-block"
+                  *ngIf="nfsForm.showError('access_type', formDir, 'required')"
+                  i18n>Required field</span>
+          </div>
+        </div>
+
+        <!-- Squash -->
+        <div class="form-group"
+             [ngClass]="{'has-error': nfsForm.showError('squash', formDir)}">
+          <label class="col-sm-3 control-label"
+                 for="squash">
+            <ng-container i18n>Squash</ng-container>
+            <span class="required"></span>
+          </label>
+          <div class="col-sm-9">
+            <select class="form-control"
+                    name="squash"
+                    formControlName="squash"
+                    id="squash">
+              <option *ngIf="nfsSquash === null"
+                      value=""
+                      i18n>Loading...</option>
+              <option *ngIf="nfsSquash !== null && nfsSquash.length === 0"
+                      value=""
+                      i18n>-- No squash available --</option>
+              <option *ngIf="nfsSquash !== null && nfsSquash.length > 0"
+                      value=""
+                      i18n>--Select what kind of user id squashing is performed --</option>
+              <option *ngFor="let squash of nfsSquash"
+                      [value]="squash">{{ squash }}</option>
+
+            </select>
+            <span class="help-block"
+                  *ngIf="nfsForm.showError('squash', formDir,'required')"
+                  i18n>Required field</span>
+          </div>
+        </div>
+
+        <!-- Transport Protocol -->
+        <div class="form-group"
+             [ngClass]="{'has-error': nfsForm.showError('transportUDP', formDir) || nfsForm.showError('transportTCP', formDir)}">
+          <label class="col-sm-3 control-label"
+                 for="transports">
+            <ng-container i18n>Transport Protocol</ng-container>
+            <span class="required"></span>
+          </label>
+          <div class="col-sm-9">
+            <div class="checkbox checkbox-primary">
+              <input type="checkbox"
+                     formControlName="transportUDP"
+                     name="transportUDP"
+                     id="transportUDP">
+              <label for="transportUDP"
+                     i18n>UDP</label>
+            </div>
+            <div class="checkbox checkbox-primary">
+              <input type="checkbox"
+                     formControlName="transportTCP"
+                     name="transportTCP"
+                     id="transportTCP">
+              <label for="transportTCP"
+                     i18n>TCP</label>
+            </div>
+            <span class="help-block"
+                  *ngIf="nfsForm.showError('transportUDP', formDir, 'required') ||
+                  nfsForm.showError('transportTCP', formDir, 'required')"
+                  i18n>Required field</span>
+            <hr>
+          </div>
+        </div>
+
+        <!-- Clients -->
+        <cd-nfs-form-client [form]="nfsForm"
+                            #nfsClients>
+        </cd-nfs-form-client>
+
+      </div>
+
+      <div class="panel-footer">
+        <div class="button-group text-right">
+          <cd-submit-button [form]="formDir"
+                            type="button"
+                            (submitAction)="submitAction()">
+            <ng-container i18n>Submit</ng-container>
+          </cd-submit-button>
+          <button type="button"
+                  class="btn btn-sm btn-default"
+                  (click)="cancelAction()"
+                  i18n>Back</button>
+        </div>
+      </div>
+    </div>
+  </form>
+</div>
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 (file)
index 0000000..cebcc88
--- /dev/null
@@ -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 (file)
index 0000000..b5bf6a7
--- /dev/null
@@ -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<NfsFormComponent>;
+  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 (file)
index 0000000..2ab7055
--- /dev/null
@@ -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<any> = Observable.create((observer: any) => {
+    observer.next(this.nfsForm.getValue('path'));
+  }).pipe(
+    mergeMap((token: string) => this.getPathTypeahead(token)),
+    map((val: any) => val.paths)
+  );
+
+  bucketDataSource: Observable<any> = 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<any> {
+    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<any>;
+    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 (file)
index 0000000..7e1cdbc
--- /dev/null
@@ -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 (file)
index 0000000..d2e176c
--- /dev/null
@@ -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')
+    );
+  }
+}
index cb5b69c0a205bfba9945cc17e83e09f1bbb75b43..c7870011592cdb716188204d0ef888f3baacf705 100644 (file)
@@ -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 {}
index 9e2791db8a7a733720bbc4be2f6553439b6233e5..f53416f6e7ffa4811c7bdbe3a684bad69074bf58 100644 (file)
           <context context-type="sourcefile">app/core/navigation/navigation/navigation.component.html</context>
           <context context-type="linenumber">47</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">21</context>
+        </context-group>
       </trans-unit><trans-unit id="099b441d49333b3c6d30b36dc0a4763e64c78920" datatype="html">
         <source>Hosts</source>
         <context-group purpose="location">
           <context context-type="sourcefile">app/core/navigation/navigation/navigation.component.html</context>
           <context context-type="linenumber">158</context>
         </context-group>
+      </trans-unit><trans-unit id="a24eabd99ea5af20f5f94c4484649cd30370042b" datatype="html">
+        <source>NFS</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/core/navigation/navigation/navigation.component.html</context>
+          <context context-type="linenumber">168</context>
+        </context-group>
       </trans-unit><trans-unit id="a4eff72d97b7ced051398d581f10968218057ddc" datatype="html">
         <source>Filesystems</source>
         <context-group purpose="location">
           <context context-type="sourcefile">app/core/navigation/navigation/navigation.component.html</context>
-          <context context-type="linenumber">169</context>
+          <context context-type="linenumber">176</context>
         </context-group>
       </trans-unit><trans-unit id="2190548d236ca5f7bc7ab2bca334b860c5ff2ad4" datatype="html">
         <source>Object Gateway</source>
         <context-group purpose="location">
           <context context-type="sourcefile">app/core/navigation/navigation/navigation.component.html</context>
-          <context context-type="linenumber">180</context>
+          <context context-type="linenumber">187</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-list/nfs-list.component.html</context>
+          <context context-type="linenumber">32</context>
         </context-group>
       </trans-unit><trans-unit id="9e24f9e2d42104ffc01599db4d566d1cc518f9e6" datatype="html">
         <source>Daemons</source>
         <context-group purpose="location">
           <context context-type="sourcefile">app/core/navigation/navigation/navigation.component.html</context>
-          <context context-type="linenumber">189</context>
+          <context context-type="linenumber">196</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">app/ceph/block/iscsi/iscsi.component.html</context>
           <context context-type="sourcefile">app/ceph/block/mirroring/overview/overview.component.html</context>
           <context context-type="linenumber">5</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">53</context>
+        </context-group>
       </trans-unit><trans-unit id="4d13a9cd5ed3dcee0eab22cb25198d43886942be" datatype="html">
         <source>Users</source>
         <context-group purpose="location">
           <context context-type="sourcefile">app/core/navigation/navigation/navigation.component.html</context>
-          <context context-type="linenumber">195</context>
+          <context context-type="linenumber">202</context>
         </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">app/core/auth/user-tabs/user-tabs.component.html</context>
         <source>Buckets</source>
         <context-group purpose="location">
           <context context-type="sourcefile">app/core/navigation/navigation/navigation.component.html</context>
-          <context context-type="linenumber">201</context>
+          <context context-type="linenumber">208</context>
         </context-group>
       </trans-unit><trans-unit id="797f8214e8148f4bf0d244baaa7341706b419549" datatype="html">
         <source>Retrieving data<x id="START_TAG_SPAN_1" ctype="x-span" equiv-text="&lt;span&gt;"/> for
           <context context-type="sourcefile">app/ceph/cluster/configuration/configuration-form/configuration-form.component.html</context>
           <context context-type="linenumber">159</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">520</context>
+        </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">app/ceph/pool/pool-form/pool-form.component.html</context>
           <context context-type="linenumber">443</context>
           <context context-type="sourcefile">app/ceph/cluster/osd/osd-scrub-modal/osd-scrub-modal.component.html</context>
           <context context-type="linenumber">21</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">515</context>
+        </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">app/ceph/block/mirroring/pool-edit-peer-modal/pool-edit-peer-modal.component.html</context>
           <context context-type="linenumber">106</context>
           <context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
           <context context-type="linenumber">139</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">32</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">106</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">139</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">171</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">204</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">418</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">453</context>
+        </context-group>
         <context-group purpose="location">
           <context context-type="sourcefile">app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form.component.html</context>
           <context context-type="linenumber">59</context>
           <context context-type="sourcefile">app/ceph/block/rbd-form/rbd-form.component.html</context>
           <context context-type="linenumber">142</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">109</context>
+        </context-group>
       </trans-unit><trans-unit id="7faaaa08f56427999f3be41df1093ce4089bbd75" datatype="html">
         <source>Size</source>
         <context-group purpose="location">
           <context context-type="sourcefile">app/ceph/cephfs/cephfs-detail/cephfs-detail.component.html</context>
           <context context-type="linenumber">3</context>
         </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-details/nfs-details.component.html</context>
+          <context context-type="linenumber">2</context>
+        </context-group>
       </trans-unit><trans-unit id="d2bcd3296d2850de762fb943060b7e086a893181" datatype="html">
         <source>Health</source>
         <context-group purpose="location">
           <context context-type="sourcefile">app/ceph/dashboard/dashboard/dashboard.component.html</context>
           <context context-type="linenumber">8</context>
         </context-group>
+      </trans-unit><trans-unit id="7ffe39df9d88c972792bd8688b215392deb8313d" datatype="html">
+        <source>Clients</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form-client/nfs-form-client.component.html</context>
+          <context context-type="linenumber">3</context>
+        </context-group>
+      </trans-unit><trans-unit id="f2dae0bda66f6a349444951c0379c28cda47d6d1" datatype="html">
+        <source>Any client can access</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form-client/nfs-form-client.component.html</context>
+          <context context-type="linenumber">11</context>
+        </context-group>
+      </trans-unit><trans-unit id="7882f2edb1d4139800b276b6b0bbf5ae0b2234ef" datatype="html">
+        <source>Addresses</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form-client/nfs-form-client.component.html</context>
+          <context context-type="linenumber">32</context>
+        </context-group>
+      </trans-unit><trans-unit id="9bd96fcf50863e685c74d0490392f46689ffbdb6" datatype="html">
+        <source>Required field</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form-client/nfs-form-client.component.html</context>
+          <context context-type="linenumber">42</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">44</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">118</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">151</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">183</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">216</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">253</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">277</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">309</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">349</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">396</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">434</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">466</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">498</context>
+        </context-group>
+      </trans-unit><trans-unit id="a5f3f74c0f6925826cb2188576391c0da01a23f0" datatype="html">
+        <source>Must contain one or more comma-separated values</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form-client/nfs-form-client.component.html</context>
+          <context context-type="linenumber">45</context>
+        </context-group>
+      </trans-unit><trans-unit id="8bb5b2073697f3f4378c44a49b7524934c9268f4" datatype="html">
+        <source>For example:</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form-client/nfs-form-client.component.html</context>
+          <context context-type="linenumber">47</context>
+        </context-group>
+      </trans-unit><trans-unit id="8f969c655b3fbe4fba7e277caf4cd2c459f9fca5" datatype="html">
+        <source>Access Type</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form-client/nfs-form-client.component.html</context>
+          <context context-type="linenumber">57</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">408</context>
+        </context-group>
+      </trans-unit><trans-unit id="28952831a284cfe2b4fc39ca610e80b52598905a" datatype="html">
+        <source>Squash</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form-client/nfs-form-client.component.html</context>
+          <context context-type="linenumber">78</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">443</context>
+        </context-group>
+      </trans-unit><trans-unit id="0660ae339068979854ade34a96546980723dede3" datatype="html">
+        <source>Add clients</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form-client/nfs-form-client.component.html</context>
+          <context context-type="linenumber">99</context>
+        </context-group>
+      </trans-unit><trans-unit id="6ecb266eacdb295d503d754e8a4a4c631efa841b" datatype="html">
+        <source>NFS export <x id="INTERPOLATION" equiv-text="{{ export_id ? cluster_id + &apos;:&apos; + export_id : &apos;&apos; }}"/></source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">10</context>
+        </context-group>
+      </trans-unit><trans-unit id="135b91a2d908d5814b782695470a6a786c99d9d2" datatype="html">
+        <source>-- No cluster available --</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">35</context>
+        </context-group>
+      </trans-unit><trans-unit id="c501dba379f566885919240ea277b5bc10c14d18" datatype="html">
+        <source>-- Select the cluster --</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">38</context>
+        </context-group>
+      </trans-unit><trans-unit id="cf85b1ee58326aa9da63da41b2629c9db7c9a5b9" datatype="html">
+        <source>Add daemon</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">81</context>
+        </context-group>
+      </trans-unit><trans-unit id="b3f6ba7fe84d6508705cdfe234f0fcc8ff85c9cf" datatype="html">
+        <source>Storage Backend</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">95</context>
+        </context-group>
+      </trans-unit><trans-unit id="b6fee356d1db954255a56d8169405a89595246b9" datatype="html">
+        <source>-- Select the storage backend --</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">112</context>
+        </context-group>
+      </trans-unit><trans-unit id="76d67035c3ab3d8e56f725859f820f03fda41cfc" datatype="html">
+        <source>Object Gateway User</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">128</context>
+        </context-group>
+      </trans-unit><trans-unit id="fade7788bace74337f306ae209f10fc187ef4671" datatype="html">
+        <source>-- No users available --</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">142</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">207</context>
+        </context-group>
+      </trans-unit><trans-unit id="6d30b7b36cf8f6364167321bdb4ba35d4cefce7b" datatype="html">
+        <source>-- Select the object gateway user --</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">145</context>
+        </context-group>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">210</context>
+        </context-group>
+      </trans-unit><trans-unit id="589ce20d3ba3e3ac44f75decfaadc4ea8f0aec2d" datatype="html">
+        <source>CephFS User ID</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">161</context>
+        </context-group>
+      </trans-unit><trans-unit id="c4b88a53ac3b0ece46ba9b3ad72355a3c190cce7" datatype="html">
+        <source>-- No clients available --</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">174</context>
+        </context-group>
+      </trans-unit><trans-unit id="da52835b80497a0002d24414b057dc46ae44ce38" datatype="html">
+        <source>-- Select the cephx client --</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">177</context>
+        </context-group>
+      </trans-unit><trans-unit id="fd3419e8957d928d7f7ba19c93356a0dbff02871" datatype="html">
+        <source>CephFS Name</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">193</context>
+        </context-group>
+      </trans-unit><trans-unit id="957512d0321f73e9f115bce1bd823fa635170c41" datatype="html">
+        <source>Security Label</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">227</context>
+        </context-group>
+      </trans-unit><trans-unit id="65ce0fa4da1ed55e658aeb31d1644a29f06bb342" datatype="html">
+        <source>Enable security label</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">239</context>
+        </context-group>
+      </trans-unit><trans-unit id="7e808f804130c7b6ff719509cbc06ebb27393a48" datatype="html">
+        <source>CephFS Path</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">263</context>
+        </context-group>
+      </trans-unit><trans-unit id="5ecc0107badb6625466aaa3f975b5c05276f432f" datatype="html">
+        <source>Path need to start with a &apos;/&apos; and can be followed by a word</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">281</context>
+        </context-group>
+      </trans-unit><trans-unit id="2d02916f44fc63e13ab16d1cbe72aa6cb51feab3" datatype="html">
+        <source>New directory will be created</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">284</context>
+        </context-group>
+      </trans-unit><trans-unit id="766c66ad5cc981c531aaf3fe3a2a7a346ddc8d83" datatype="html">
+        <source>Path</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">294</context>
+        </context-group>
+      </trans-unit><trans-unit id="7ec35c722a50b976620f22612f7be619c12ceb90" datatype="html">
+        <source>Path can only be a single &apos;/&apos; or a word</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">313</context>
+        </context-group>
+      </trans-unit><trans-unit id="aebb6a5090c24511de4530195694bb3f3dcf0342" datatype="html">
+        <source>New bucket will be created</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">317</context>
+        </context-group>
+      </trans-unit><trans-unit id="bee6900143996c0e908a10564532eba3da0b30fb" datatype="html">
+        <source>NFS Protocol</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">326</context>
+        </context-group>
+      </trans-unit><trans-unit id="2f534178c01ebf1307da2eaeef04bc6801ebc729" datatype="html">
+        <source>NFSv3</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">336</context>
+        </context-group>
+      </trans-unit><trans-unit id="f5043c0921e709935ab026bb3253ffe1f159fca1" datatype="html">
+        <source>NFSv4</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">344</context>
+        </context-group>
+      </trans-unit><trans-unit id="92488963d23095985a47c0d6e62304e11d333f19" datatype="html">
+        <source>NFS Tag</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">358</context>
+        </context-group>
+      </trans-unit><trans-unit id="aae93362720aea94623682996dd3fcd0f906f056" datatype="html">
+        <source>Alternative access for <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="&lt;strong&gt;"/>NFS v3<x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="&lt;/strong&gt;"/> mounts (it must not have a leading /).</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">360</context>
+        </context-group>
+      </trans-unit><trans-unit id="45d6db77dcf1a3eeb921033abc7882e517a541cc" datatype="html">
+        <source>Clients may not mount subdirectories (i.e. if Tag = foo, the client may not mount foo/baz).</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">361</context>
+        </context-group>
+      </trans-unit><trans-unit id="a1c7a8676b55e882a97c6a6fb205204f9c761afa" datatype="html">
+        <source>By using different Tag options, the same Path may be exported multiple times.</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">362</context>
+        </context-group>
+      </trans-unit><trans-unit id="6d2c39708a32910f89701dd7e1cfb9ec1c195768" datatype="html">
+        <source>Pseudo</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">380</context>
+        </context-group>
+      </trans-unit><trans-unit id="1f8be2ae25947bec0b84c2338201580ea053f34e" datatype="html">
+        <source>The position that this <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="&lt;strong&gt;"/>NFS v4<x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="&lt;/strong&gt;"/> export occupies
+                in the <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="&lt;strong&gt;"/>Pseudo FS<x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="&lt;/strong&gt;"/> (it must be unique).</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">383</context>
+        </context-group>
+      </trans-unit><trans-unit id="f3af55f7fd5b1d9e5a53e030c80116dc635bfb9f" datatype="html">
+        <source>By using different Pseudo options, the same Path may be exported multiple times.</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">385</context>
+        </context-group>
+      </trans-unit><trans-unit id="a90eaec5c5d9b248bcc5a9765c21d80a5d210765" datatype="html">
+        <source>Wrong format</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">399</context>
+        </context-group>
+      </trans-unit><trans-unit id="27eb35c4b4ac08781a7253a2ab40f8f7d957ba51" datatype="html">
+        <source>-- No access type available --</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">421</context>
+        </context-group>
+      </trans-unit><trans-unit id="509ce016c9110a54028dafd741f15ceacbe74b5a" datatype="html">
+        <source>-- Select the access type --</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">424</context>
+        </context-group>
+      </trans-unit><trans-unit id="4deda03573eaaff77e63f6a238a1f0ca7816950a" datatype="html">
+        <source>-- No squash available --</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">456</context>
+        </context-group>
+      </trans-unit><trans-unit id="a0e82a4da88e7fdf270444f838d45849676e9d4b" datatype="html">
+        <source>--Select what kind of user id squashing is performed --</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">459</context>
+        </context-group>
+      </trans-unit><trans-unit id="d01b7c3f7f06712c53d054cfbe4f53d446b038e8" datatype="html">
+        <source>Transport Protocol</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">475</context>
+        </context-group>
+      </trans-unit><trans-unit id="d2a6ad6e8bc315f07911722c05767ac79c136d99" datatype="html">
+        <source>UDP</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">485</context>
+        </context-group>
+      </trans-unit><trans-unit id="9c030f11e0aae9b24d2c048c57f29f590be621df" datatype="html">
+        <source>TCP</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-form/nfs-form.component.html</context>
+          <context context-type="linenumber">493</context>
+        </context-group>
+      </trans-unit><trans-unit id="3bd5ad3144184479c23ec67ceda089f9883840aa" datatype="html">
+        <source>To apply any changes you have to restart the Ganisha services.</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-list/nfs-list.component.html</context>
+          <context context-type="linenumber">4</context>
+        </context-group>
+      </trans-unit><trans-unit id="5a9de53b931d4b73b84f3fd729abfbbb20a9619e" datatype="html">
+        <source>Orchestrator not available</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-list/nfs-list.component.html</context>
+          <context context-type="linenumber">2</context>
+        </context-group>
+      </trans-unit><trans-unit id="734c9905951a774870497c5aaae8e3ee833b6196" datatype="html">
+        <source>CephFS</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">app/ceph/nfs/nfs-list/nfs-list.component.html</context>
+          <context context-type="linenumber">30</context>
+        </context-group>
       </trans-unit><trans-unit id="594cd9429597cf6bede5560b3d8fe578821213de" datatype="html">
         <source>Add erasure code profile</source>
         <context-group purpose="location">
           <context context-type="linenumber">1</context>
         </context-group>
       </trans-unit>
+      <trans-unit id="af1396bdc49f45ea6c4a1e414769f5e2a382c834" datatype="html">
+        <source>Transport</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/ceph/nfs/nfs-details/nfs-details.component.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="b6a0e176d96c4feed2e975a039c044fcf822e152" datatype="html">
+        <source>CephFS User</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/ceph/nfs/nfs-details/nfs-details.component.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="44dc610cf201a163e8c38da810acec9596930bb3" datatype="html">
+        <source>CephFS Filesystem</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/ceph/nfs/nfs-details/nfs-details.component.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="db6dc7124be83c7055cb0f2719e31f2f9d46fd3d" datatype="html">
+        <source>(inherited from global config)</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="e1f97bb86f991553ec7b535cb39e7eaa99dfcfe2" datatype="html">
+        <source>inherited from global config</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="e751800766277e7c2edd652cec7a7a1a68a37852" datatype="html">
+        <source>-- Select what kind of user id squashing is performed --</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/ceph/nfs/nfs-form-client/nfs-form-client.component.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="4b58387f911e8a7b135a0c4d76c70335c65bbb32" datatype="html">
+        <source>There are no daemons available.</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/ceph/nfs/nfs-form/nfs-form.component.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="5f19586aba912ec6eb123ec5ab609a3bcc073066" datatype="html">
+        <source>Export</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/ceph/nfs/nfs-list/nfs-list.component.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="5049e204c14c648691ac775a64fb504467aeb549" datatype="html">
         <source>Value</source>
         <context-group purpose="location">
           <context context-type="linenumber">1</context>
         </context-group>
       </trans-unit>
+      <trans-unit id="c8f10184a8433d132ede0a2c0c1aa96f291cacfa" datatype="html">
+        <source>Allows all operations</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/api/nfs.service.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="82077ee97c11ee76efc3adf253b7b26654544317" datatype="html">
+        <source>Allows only operations that do not modify the server</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/api/nfs.service.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="22b5212ec10449dbf0a0af0ad34122eac51b5f2a" datatype="html">
+        <source>Does not allow read or write operations, but allows any other operation</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/api/nfs.service.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="9799d0f8c4b7727ae239fedfe3c0ce127feb7a50" datatype="html">
+        <source>Does not allow read, write, or any operation that modifies file        attributes or directory content</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/api/nfs.service.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+      </trans-unit>
+      <trans-unit id="aeddefe0af87025d8f00699aa94bed8216a99a58" datatype="html">
+        <source>Allows no access at all</source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/api/nfs.service.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+      </trans-unit>
       <trans-unit id="a436c6a4025a749198e93cac239de8deede72211" datatype="html">
         <source>-- Select the priority --</source>
         <context-group purpose="location">
           <context context-type="linenumber">1</context>
         </context-group>
       </trans-unit>
+      <trans-unit id="cf41310917cb19b335e5a950972bbff2346f7a47" datatype="html">
+        <source>NFS <x id="INTERPOLATION" equiv-text="{{nfs_id}}"/></source>
+        <context-group purpose="location">
+          <context context-type="sourcefile">src/app/shared/services/task-message.service.ts</context>
+          <context context-type="linenumber">1</context>
+        </context-group>
+      </trans-unit>
     </body>
   </file>
 </xliff>