]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: add config options form
authorTatjana Dehler <tdehler@suse.com>
Mon, 25 Jun 2018 14:30:16 +0000 (16:30 +0200)
committerTatjana Dehler <tdehler@suse.com>
Tue, 9 Oct 2018 12:50:47 +0000 (14:50 +0200)
Fixes: http://tracker.ceph.com/issues/24455
Signed-off-by: Tatjana Dehler <tdehler@suse.com>
src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.model.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/configuration.service.ts

index efe4ed58db89d92be2fee9679d0dc8cf63d60fc1..84af029ac4c0d6a19a6d19a3d04c2d01a7731595 100644 (file)
@@ -6,6 +6,7 @@ import { MirroringComponent } from './ceph/block/mirroring/mirroring.component';
 import { RbdFormComponent } from './ceph/block/rbd-form/rbd-form.component';
 import { RbdImagesComponent } from './ceph/block/rbd-images/rbd-images.component';
 import { CephfsListComponent } from './ceph/cephfs/cephfs-list/cephfs-list.component';
+import { ConfigurationFormComponent } from './ceph/cluster/configuration/configuration-form/configuration-form.component';
 import { ConfigurationComponent } from './ceph/cluster/configuration/configuration.component';
 import { HostsComponent } from './ceph/cluster/hosts/hosts.component';
 import { MonitorComponent } from './ceph/cluster/monitor/monitor.component';
@@ -77,9 +78,15 @@ const routes: Routes = [
   },
   {
     path: 'configuration',
-    component: ConfigurationComponent,
-    canActivate: [AuthGuardService],
-    data: { breadcrumbs: 'Cluster/Configuration Documentation' }
+    data: { breadcrumbs: 'Cluster/Configuration Documentation' },
+    children: [
+      { path: '', component: ConfigurationComponent },
+      {
+        path: 'edit/:name',
+        component: ConfigurationFormComponent,
+        data: { breadcrumbs: 'Edit' }
+      }
+    ]
   },
   {
     path: 'perf_counters/:type/:id',
index 563871f53212df52c4d1f087f9ca07ec09dc9a09..748568eb768f0633d97af600d17f65b20021e723 100644 (file)
@@ -10,6 +10,7 @@ import { TabsModule } from 'ngx-bootstrap/tabs';
 import { SharedModule } from '../../shared/shared.module';
 import { PerformanceCounterModule } from '../performance-counter/performance-counter.module';
 import { ConfigurationDetailsComponent } from './configuration/configuration-details/configuration-details.component';
+import { ConfigurationFormComponent } from './configuration/configuration-form/configuration-form.component';
 import { ConfigurationComponent } from './configuration/configuration.component';
 import { HostDetailsComponent } from './hosts/host-details/host-details.component';
 import { HostsComponent } from './hosts/hosts.component';
@@ -43,7 +44,8 @@ import { OsdScrubModalComponent } from './osd/osd-scrub-modal/osd-scrub-modal.co
     OsdScrubModalComponent,
     OsdFlagsModalComponent,
     HostDetailsComponent,
-    ConfigurationDetailsComponent
+    ConfigurationDetailsComponent,
+    ConfigurationFormComponent
   ]
 })
 export class ClusterModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.html
new file mode 100644 (file)
index 0000000..74edf15
--- /dev/null
@@ -0,0 +1,167 @@
+<div class="col-sm-12 col-lg-6">
+  <form name="configForm"
+        class="form-horizontal"
+        #formDir="ngForm"
+        [formGroup]="configForm"
+        novalidate>
+    <div class="panel panel-default">
+      <div class="panel-heading">
+        <h3 class="panel-title">
+          <ng-container i18>Edit</ng-container> {{ configForm.getValue('name') }}
+        </h3>
+      </div>
+      <div class="panel-body">
+
+        <!-- Name -->
+        <div class="form-group">
+          <label i18n
+                 class="control-label col-sm-3">Name
+          </label>
+          <div class="col-sm-9">
+            <input class="form-control"
+                   type="text"
+                   id="name"
+                   formControlName="name"
+                   readonly>
+          </div>
+        </div>
+
+        <!-- Description -->
+        <div class="form-group"
+             *ngIf="configForm.getValue('desc')">
+          <label i18n
+                 class="control-label col-sm-3">Description
+          </label>
+          <div class="col-sm-9">
+            <textarea class="form-control"
+                      id="desc"
+                      formControlName="desc"
+                      readonly>
+            </textarea>
+          </div>
+        </div>
+
+        <!-- Long description -->
+        <div class="form-group"
+             *ngIf="configForm.getValue('long_desc')">
+          <label i18n
+                 class="control-label col-sm-3">Long description
+          </label>
+          <div class="col-sm-9">
+            <textarea class="form-control"
+                      id="long_desc"
+                      formControlName="long_desc"
+                      readonly>
+            </textarea>
+          </div>
+        </div>
+
+        <!-- Default -->
+        <div class="form-group"
+             *ngIf="configForm.getValue('default') !== ''">
+          <label i18n
+                 class="control-label col-sm-3">Default
+          </label>
+          <div class="col-sm-9">
+            <input class="form-control"
+                   type="text"
+                   id="default"
+                   formControlName="default"
+                   readonly>
+          </div>
+        </div>
+
+        <!-- Daemon default -->
+        <div class="form-group"
+             *ngIf="configForm.getValue('daemon_default') !== ''">
+          <label i18n
+                 class="control-label col-sm-3">Daemon default
+          </label>
+          <div class="col-sm-9">
+            <input class="form-control"
+                   type="text"
+                   id="daemon_default"
+                   formControlName="daemon_default"
+                   readonly>
+          </div>
+        </div>
+
+        <!-- Services -->
+        <div class="form-group"
+             *ngIf="configForm.getValue('services').length > 0">
+          <label i18n
+                 class="control-label col-sm-3">Services
+          </label>
+          <div class="col-sm-9">
+            <span *ngFor="let service of configForm.getValue('services')"
+                  class="form-component-badge">
+              <span class="badge badge-pill badge-primary">{{ service }}</span>
+            </span>
+          </div>
+        </div>
+
+        <!-- Values -->
+        <div class="col-sm-12">
+          <h2 i18n
+              class="page-header">Values</h2>
+          <div class="row" *ngFor="let section of availSections">
+            <div class="form-group" *ngIf="type === 'bool'">
+              <div class="col-sm-offset-3 col-sm-9">
+                <div class="checkbox checkbox-primary">
+                  <input [id]="section"
+                         type="checkbox"
+                         [formControlName]="section">
+                  <label [for]="section"
+                         i18n>{{ section }}
+                  </label>
+                </div>
+              </div>
+            </div>
+            <div class="form-group"
+                 [ngClass]="{'has-error': configForm.showError(section, formDir)}"
+                 *ngIf="type !== 'bool'">
+              <label class="control-label col-sm-3"
+                     [for]="section"
+                     i18n>{{ section }}
+              </label>
+              <div class="col-sm-9">
+                <input class="form-control"
+                       [type]="inputType"
+                       [id]="section"
+                       [placeholder]="humanReadableType"
+                       [formControlName]="section"
+                       [step]="getStep(type, this.configForm.getValue(section))">
+                <span class="help-block"
+                      *ngIf="configForm.showError(section, formDir, 'pattern')"
+                      i18n>
+                  {{ patternHelpText }}
+                </span>
+                <span class="help-block"
+                      *ngIf="configForm.showError(section, formDir, 'max')"
+                      i18n>
+                  The entered value is too high! It must not be greater than {{ maxValue }}.
+                </span>
+                <span class="help-block"
+                      *ngIf="configForm.showError(section, formDir, 'min')"
+                      i18n>
+                  The entered value is too low! It must not be lower than {{ minValue }}.
+                </span>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+      <!-- Footer -->
+      <div class="panel-footer">
+        <div class="button-group text-right">
+          <button type="button"
+                  class="btn btn-sm btn-default"
+                  routerLink="/configuration"
+                  i18n>
+            Back
+          </button>
+        </div>
+      </div>
+    </div>
+  </form>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.scss
new file mode 100644 (file)
index 0000000..c9a160d
--- /dev/null
@@ -0,0 +1,8 @@
+.form-component-badge {
+  height: 34px;
+  display: block;
+
+  span {
+    margin-top: 7px;
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.spec.ts
new file mode 100644 (file)
index 0000000..4b98d7f
--- /dev/null
@@ -0,0 +1,275 @@
+import { HttpClientTestingModule } 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 { configureTestBed } from '../../../../../testing/unit-test-helper';
+import { SharedModule } from '../../../../shared/shared.module';
+import { ConfigurationFormComponent } from './configuration-form.component';
+import { ConfigFormModel } from './configuration-form.model';
+
+describe('ConfigurationFormComponent', () => {
+  let component: ConfigurationFormComponent;
+  let fixture: ComponentFixture<ConfigurationFormComponent>;
+  let activatedRoute: ActivatedRoute;
+
+  configureTestBed({
+    imports: [HttpClientTestingModule, ReactiveFormsModule, RouterTestingModule, SharedModule],
+    declarations: [ConfigurationFormComponent],
+    providers: [
+      {
+        provide: ActivatedRoute
+      }
+    ]
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(ConfigurationFormComponent);
+    component = fixture.componentInstance;
+    activatedRoute = TestBed.get(ActivatedRoute);
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  describe('getType', () => {
+    it('should return uint64_t type', () => {
+      const ret = component.getType('uint64_t');
+      expect(ret).toBeTruthy();
+      expect(ret.name).toBe('uint64_t');
+      expect(ret.inputType).toBe('number');
+      expect(ret.humanReadable).toBe('Positive integer value');
+      expect(ret.defaultMin).toBe(0);
+      expect(ret.patternHelpText).toBe('The entered value needs to be a positive number.');
+      expect(ret.isNumberType).toBe(true);
+      expect(ret.allowsNegative).toBe(false);
+    });
+
+    it('should return int64_t type', () => {
+      const ret = component.getType('int64_t');
+      expect(ret).toBeTruthy();
+      expect(ret.name).toBe('int64_t');
+      expect(ret.inputType).toBe('number');
+      expect(ret.humanReadable).toBe('Integer value');
+      expect(ret.defaultMin).toBeUndefined();
+      expect(ret.patternHelpText).toBe('The entered value needs to be a number.');
+      expect(ret.isNumberType).toBe(true);
+      expect(ret.allowsNegative).toBe(true);
+    });
+
+    it('should return size_t type', () => {
+      const ret = component.getType('size_t');
+      expect(ret).toBeTruthy();
+      expect(ret.name).toBe('size_t');
+      expect(ret.inputType).toBe('number');
+      expect(ret.humanReadable).toBe('Positive integer value (size)');
+      expect(ret.defaultMin).toBe(0);
+      expect(ret.patternHelpText).toBe('The entered value needs to be a positive number.');
+      expect(ret.isNumberType).toBe(true);
+      expect(ret.allowsNegative).toBe(false);
+    });
+
+    it('should return secs type', () => {
+      const ret = component.getType('secs');
+      expect(ret).toBeTruthy();
+      expect(ret.name).toBe('secs');
+      expect(ret.inputType).toBe('number');
+      expect(ret.humanReadable).toBe('Positive integer value (secs)');
+      expect(ret.defaultMin).toBe(1);
+      expect(ret.patternHelpText).toBe('The entered value needs to be a positive number.');
+      expect(ret.isNumberType).toBe(true);
+      expect(ret.allowsNegative).toBe(false);
+    });
+
+    it('should return double type', () => {
+      const ret = component.getType('double');
+      expect(ret).toBeTruthy();
+      expect(ret.name).toBe('double');
+      expect(ret.inputType).toBe('number');
+      expect(ret.humanReadable).toBe('Decimal value');
+      expect(ret.defaultMin).toBeUndefined();
+      expect(ret.patternHelpText).toBe('The entered value needs to be a number or decimal.');
+      expect(ret.isNumberType).toBe(true);
+      expect(ret.allowsNegative).toBe(true);
+    });
+
+    it('should return std::string type', () => {
+      const ret = component.getType('std::string');
+      expect(ret).toBeTruthy();
+      expect(ret.name).toBe('std::string');
+      expect(ret.inputType).toBe('text');
+      expect(ret.humanReadable).toBe('Text');
+      expect(ret.defaultMin).toBeUndefined();
+      expect(ret.patternHelpText).toBeUndefined();
+      expect(ret.isNumberType).toBe(false);
+      expect(ret.allowsNegative).toBeUndefined();
+    });
+
+    it('should return entity_addr_t type', () => {
+      const ret = component.getType('entity_addr_t');
+      expect(ret).toBeTruthy();
+      expect(ret.name).toBe('entity_addr_t');
+      expect(ret.inputType).toBe('text');
+      expect(ret.humanReadable).toBe('IPv4 or IPv6 address');
+      expect(ret.defaultMin).toBeUndefined();
+      expect(ret.patternHelpText).toBe('The entered value needs to be a valid IP address.');
+      expect(ret.isNumberType).toBe(false);
+      expect(ret.allowsNegative).toBeUndefined();
+    });
+
+    it('should return uuid_d type', () => {
+      const ret = component.getType('uuid_d');
+      expect(ret).toBeTruthy();
+      expect(ret.name).toBe('uuid_d');
+      expect(ret.inputType).toBe('text');
+      expect(ret.humanReadable).toBe('UUID');
+      expect(ret.defaultMin).toBeUndefined();
+      expect(ret.patternHelpText).toBe(
+        'The entered value is not a valid UUID, e.g.: 67dcac9f-2c03-4d6c-b7bd-1210b3a259a8'
+      );
+      expect(ret.isNumberType).toBe(false);
+      expect(ret.allowsNegative).toBeUndefined();
+    });
+
+    it('should return bool type', () => {
+      const ret = component.getType('bool');
+      expect(ret).toBeTruthy();
+      expect(ret.name).toBe('bool');
+      expect(ret.inputType).toBe('checkbox');
+      expect(ret.humanReadable).toBe('Boolean value');
+      expect(ret.defaultMin).toBeUndefined();
+      expect(ret.patternHelpText).toBeUndefined();
+      expect(ret.isNumberType).toBe(false);
+      expect(ret.allowsNegative).toBeUndefined();
+    });
+
+    it('should throw an error for unknown type', () => {
+      expect(() =>
+        component.getType('unknown').toThrowError('Found unknown type "unknown" for config option.')
+      );
+    });
+  });
+
+  describe('getValidators', () => {
+    it('should return a validator for types double, entity_addr_t and uuid_d', () => {
+      const types = ['double', 'entity_addr_t', 'uuid_d'];
+
+      types.forEach((valType) => {
+        const configOption = new ConfigFormModel();
+        configOption.type = valType;
+
+        const ret = component.getValidators(configOption);
+        expect(ret).toBeTruthy();
+        expect(ret.length).toBe(1);
+      });
+    });
+
+    it('should not return a validator for types std::string and bool', () => {
+      const types = ['std::string', 'bool'];
+
+      types.forEach((valType) => {
+        const configOption = new ConfigFormModel();
+        configOption.type = valType;
+
+        const ret = component.getValidators(configOption);
+        expect(ret).toBeUndefined();
+      });
+    });
+
+    it('should return a pattern and a min validator', () => {
+      const configOption = new ConfigFormModel();
+      configOption.type = 'int64_t';
+      configOption.min = 2;
+
+      const ret = component.getValidators(configOption);
+      expect(ret).toBeTruthy();
+      expect(ret.length).toBe(2);
+      expect(component.minValue).toBe(2);
+      expect(component.maxValue).toBeUndefined();
+    });
+
+    it('should return a pattern and a max validator', () => {
+      const configOption = new ConfigFormModel();
+      configOption.type = 'int64_t';
+      configOption.max = 5;
+
+      const ret = component.getValidators(configOption);
+      expect(ret).toBeTruthy();
+      expect(ret.length).toBe(2);
+      expect(component.minValue).toBeUndefined();
+      expect(component.maxValue).toBe(5);
+    });
+
+    it('should return multiple validators', () => {
+      const configOption = new ConfigFormModel();
+      configOption.type = 'double';
+      configOption.max = 5.2;
+      configOption.min = 1.5;
+
+      const ret = component.getValidators(configOption);
+      expect(ret).toBeTruthy();
+      expect(ret.length).toBe(3);
+      expect(component.minValue).toBe(1.5);
+      expect(component.maxValue).toBe(5.2);
+    });
+  });
+
+  describe('getStep', () => {
+    it('should return the correct step for type uint64_t and value 0', () => {
+      const ret = component.getStep('uint64_t', 0);
+      expect(ret).toBe(1);
+    });
+
+    it('should return the correct step for type int64_t and value 1', () => {
+      const ret = component.getStep('int64_t', 1);
+      expect(ret).toBe(1);
+    });
+
+    it('should return the correct step for type int64_t and value null', () => {
+      const ret = component.getStep('int64_t', null);
+      expect(ret).toBe(1);
+    });
+
+    it('should return the correct step for type size_t and value 2', () => {
+      const ret = component.getStep('size_t', 2);
+      expect(ret).toBe(1);
+    });
+
+    it('should return the correct step for type secs and value 3', () => {
+      const ret = component.getStep('secs', 3);
+      expect(ret).toBe(1);
+    });
+
+    it('should return the correct step for type double and value 1', () => {
+      const ret = component.getStep('double', 1);
+      expect(ret).toBe(0.1);
+    });
+
+    it('should return the correct step for type double and value 0.1', () => {
+      const ret = component.getStep('double', 0.1);
+      expect(ret).toBe(0.1);
+    });
+
+    it('should return the correct step for type double and value 0.02', () => {
+      const ret = component.getStep('double', 0.02);
+      expect(ret).toBe(0.01);
+    });
+
+    it('should return the correct step for type double and value 0.003', () => {
+      const ret = component.getStep('double', 0.003);
+      expect(ret).toBe(0.001);
+    });
+
+    it('should return the correct step for type double and value null', () => {
+      const ret = component.getStep('double', null);
+      expect(ret).toBe(0.1);
+    });
+
+    it('should return undefined for unknown type', () => {
+      const ret = component.getStep('unknown', 1);
+      expect(ret).toBeUndefined();
+    });
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.component.ts
new file mode 100644 (file)
index 0000000..2e654db
--- /dev/null
@@ -0,0 +1,242 @@
+import { Component, OnInit } from '@angular/core';
+import { FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+
+import { ConfigurationService } from '../../../../shared/api/configuration.service';
+import { CdFormGroup } from '../../../../shared/forms/cd-form-group';
+import { CdValidators } from '../../../../shared/forms/cd-validators';
+import { ConfigFormModel } from './configuration-form.model';
+
+@Component({
+  selector: 'cd-configuration-form',
+  templateUrl: './configuration-form.component.html',
+  styleUrls: ['./configuration-form.component.scss']
+})
+export class ConfigurationFormComponent implements OnInit {
+  configForm: CdFormGroup;
+  response: ConfigFormModel;
+  type: string;
+  inputType: string;
+  humanReadableType: string;
+  minValue: number;
+  maxValue: number;
+  patternHelpText: string;
+  availSections = ['global', 'mon', 'mgr', 'osd', 'mds', 'client'];
+
+  constructor(
+    private route: ActivatedRoute,
+    private router: Router,
+    private configService: ConfigurationService
+  ) {
+    this.createForm();
+  }
+
+  createForm() {
+    const formControls = {
+      name: new FormControl({ value: null }),
+      desc: new FormControl({ value: null }),
+      long_desc: new FormControl({ value: null }),
+      values: new FormGroup({}),
+      default: new FormControl({ value: null }),
+      daemon_default: new FormControl({ value: null }),
+      services: new FormControl([])
+    };
+
+    this.availSections.forEach((section) => {
+      formControls.values.controls[section] = new FormControl(null);
+    });
+
+    this.configForm = new CdFormGroup(formControls);
+    this.configForm._filterValue = (value) => {
+      return value;
+    };
+  }
+
+  ngOnInit() {
+    this.route.params.subscribe((params: { name: string }) => {
+      const configName = params.name;
+      this.configService.get(configName).subscribe((resp: ConfigFormModel) => {
+        this.setResponse(resp);
+      });
+    });
+  }
+
+  getType(type: string): any {
+    const knownTypes = [
+      {
+        name: 'uint64_t',
+        inputType: 'number',
+        humanReadable: 'Positive integer value',
+        defaultMin: 0,
+        patternHelpText: 'The entered value needs to be a positive number.',
+        isNumberType: true,
+        allowsNegative: false
+      },
+      {
+        name: 'int64_t',
+        inputType: 'number',
+        humanReadable: 'Integer value',
+        patternHelpText: 'The entered value needs to be a number.',
+        isNumberType: true,
+        allowsNegative: true
+      },
+      {
+        name: 'size_t',
+        inputType: 'number',
+        humanReadable: 'Positive integer value (size)',
+        defaultMin: 0,
+        patternHelpText: 'The entered value needs to be a positive number.',
+        isNumberType: true,
+        allowsNegative: false
+      },
+      {
+        name: 'secs',
+        inputType: 'number',
+        humanReadable: 'Positive integer value (secs)',
+        defaultMin: 1,
+        patternHelpText: 'The entered value needs to be a positive number.',
+        isNumberType: true,
+        allowsNegative: false
+      },
+      {
+        name: 'double',
+        inputType: 'number',
+        humanReadable: 'Decimal value',
+        patternHelpText: 'The entered value needs to be a number or decimal.',
+        isNumberType: true,
+        allowsNegative: true
+      },
+      { name: 'std::string', inputType: 'text', humanReadable: 'Text', isNumberType: false },
+      {
+        name: 'entity_addr_t',
+        inputType: 'text',
+        humanReadable: 'IPv4 or IPv6 address',
+        patternHelpText: 'The entered value needs to be a valid IP address.',
+        isNumberType: false
+      },
+      {
+        name: 'uuid_d',
+        inputType: 'text',
+        humanReadable: 'UUID',
+        patternHelpText:
+          'The entered value is not a valid UUID, e.g.: 67dcac9f-2c03-4d6c-b7bd-1210b3a259a8',
+        isNumberType: false
+      },
+      { name: 'bool', inputType: 'checkbox', humanReadable: 'Boolean value', isNumberType: false }
+    ];
+
+    let currentType = null;
+
+    knownTypes.forEach((knownType) => {
+      if (knownType.name === type) {
+        currentType = knownType;
+      }
+    });
+
+    if (currentType !== null) {
+      return currentType;
+    }
+
+    throw new Error('Found unknown type "' + type + '" for config option.');
+  }
+
+  getValidators(configOption: any): ValidatorFn[] {
+    const typeParams = this.getType(configOption.type);
+    this.patternHelpText = typeParams.patternHelpText;
+
+    if (typeParams.isNumberType) {
+      const validators = [];
+
+      if (configOption.max && configOption.max !== '') {
+        this.maxValue = configOption.max;
+        validators.push(Validators.max(configOption.max));
+      }
+
+      if ('min' in configOption && configOption.min !== '') {
+        this.minValue = configOption.min;
+        validators.push(Validators.min(configOption.min));
+      } else if ('defaultMin' in typeParams) {
+        this.minValue = typeParams.defaultMin;
+        validators.push(Validators.min(typeParams.defaultMin));
+      }
+
+      if (configOption.type === 'double') {
+        validators.push(CdValidators.decimalNumber());
+      } else {
+        validators.push(CdValidators.number(typeParams.allowsNegative));
+      }
+
+      return validators;
+    } else if (configOption.type === 'entity_addr_t') {
+      return [CdValidators.ip()];
+    } else if (configOption.type === 'uuid_d') {
+      return [CdValidators.uuid()];
+    }
+  }
+
+  getStep(type: string, value: number): number | undefined {
+    const numberTypes = ['uint64_t', 'int64_t', 'size_t', 'secs'];
+
+    if (numberTypes.includes(type)) {
+      return 1;
+    }
+
+    if (type === 'double') {
+      if (value !== null) {
+        const stringVal = value.toString();
+        if (stringVal.indexOf('.') !== -1) {
+          // Value type double and contains decimal characters
+          const decimal = value.toString().split('.');
+          return Math.pow(10, -decimal[1].length);
+        }
+      }
+
+      return 0.1;
+    }
+
+    return undefined;
+  }
+
+  setResponse(response: ConfigFormModel) {
+    this.response = response;
+    const validators = this.getValidators(response);
+
+    this.configForm.get('name').setValue(response.name);
+    this.configForm.get('desc').setValue(response.desc);
+    this.configForm.get('long_desc').setValue(response.long_desc);
+    this.configForm.get('default').setValue(response.default);
+    this.configForm.get('daemon_default').setValue(response.daemon_default);
+    this.configForm.get('services').setValue(response.services);
+
+    if (this.response.value) {
+      this.response.value.forEach((value) => {
+        // Check value type. If it's a boolean value we need to convert it because otherwise we
+        // would use the string representation. That would cause issues for e.g. checkboxes.
+        let sectionValue = null;
+        if (value.value === 'true') {
+          sectionValue = true;
+        } else if (value.value === 'false') {
+          sectionValue = false;
+        } else {
+          sectionValue = value.value;
+        }
+        this.configForm
+          .get('values')
+          .get(value.section)
+          .setValue(sectionValue);
+      });
+    }
+
+    this.availSections.forEach((section) => {
+      this.configForm
+        .get('values')
+        .get(section)
+        .setValidators(validators);
+    });
+
+    const currentType = this.getType(response.type);
+    this.type = currentType.name;
+    this.inputType = currentType.inputType;
+    this.humanReadableType = currentType.humanReadable;
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.model.ts
new file mode 100644 (file)
index 0000000..d3ebc5f
--- /dev/null
@@ -0,0 +1,12 @@
+export class ConfigFormModel {
+  name: string;
+  desc: string;
+  long_desc: string;
+  type: string;
+  value: Array<any>;
+  default: any;
+  daemon_default: any;
+  min: any;
+  max: any;
+  services: Array<string>;
+}
index b7012699ece28da3295d9b52af42ce5ac483ca3f..88b4a2a3e4e21a4143f43bcb91e867b4e38ef93a 100644 (file)
@@ -3,7 +3,12 @@
           [columns]="columns"
           selectionType="single"
           (updateSelection)="updateSelection($event)">
-  <div class="table-actions form-inline">
+  <cd-table-actions class="table-actions"
+                    [permission]="permission"
+                    [selection]="selection"
+                    [tableActions]="tableActions">
+  </cd-table-actions>
+  <div class="table-filters">
     <div class="form-group filter"
          *ngFor="let filter of filters">
       <label>{{ filter.label }}: </label>
index bf82c9583ba9284ec4fa46fd71072bbf801fd40d..a9a983e647b102e37e813d930bb04701c3ed6559 100644 (file)
@@ -1,9 +1,12 @@
 import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
 
 import { ConfigurationService } from '../../../shared/api/configuration.service';
+import { CdTableAction } from '../../../shared/models/cd-table-action';
 import { CdTableColumn } from '../../../shared/models/cd-table-column';
 import { CdTableFetchDataContext } from '../../../shared/models/cd-table-fetch-data-context';
 import { CdTableSelection } from '../../../shared/models/cd-table-selection';
+import { Permission } from '../../../shared/models/permissions';
+import { AuthStorageService } from '../../../shared/services/auth-storage.service';
 
 @Component({
   selector: 'cd-configuration',
@@ -11,6 +14,8 @@ import { CdTableSelection } from '../../../shared/models/cd-table-selection';
   styleUrls: ['./configuration.component.scss']
 })
 export class ConfigurationComponent implements OnInit {
+  permission: Permission;
+  tableActions: CdTableAction[];
   data = [];
   columns: CdTableColumn[];
   selection = new CdTableSelection();
@@ -79,7 +84,21 @@ export class ConfigurationComponent implements OnInit {
   @ViewChild('confFlagTpl')
   public confFlagTpl: TemplateRef<any>;
 
-  constructor(private configurationService: ConfigurationService) {}
+  constructor(
+    private authStorageService: AuthStorageService,
+    private configurationService: ConfigurationService
+  ) {
+    this.permission = this.authStorageService.getPermissions().configOpt;
+    const getConfigOptUri = () =>
+      this.selection.first() && `${encodeURI(this.selection.first().name)}`;
+    const editAction: CdTableAction = {
+      permission: 'update',
+      icon: 'fa-pencil',
+      routerLink: () => `/configuration/edit/${getConfigOptUri()}`,
+      name: 'Edit'
+    };
+    this.tableActions = [editAction];
+  }
 
   ngOnInit() {
     this.columns = [
index 5dd305cdc865de461a176f6f8b622df5c1e38702..dfc68eba2da9a6ca140ad2fff911e1902cf5ce50 100644 (file)
@@ -12,4 +12,8 @@ export class ConfigurationService {
   getConfigData() {
     return this.http.get('api/cluster_conf/');
   }
+
+  get(configOption) {
+    return this.http.get(`api/cluster_conf/${configOption}`);
+  }
 }