type="button"
data-toggle="button"
[ngClass]="{'active': isDisabled(option.name)}"
- tooltip="Remove the local configuration value. The parent configuration value will be inherited and used instead."
- containerClass="tooltip-wide"
- [delay]="1000"
- i18n-tooltip
+ title="Remove the local configuration value. The parent configuration value will be inherited and used instead."
+ i18n-title
(click)="reset(option.name)">
<i class="fa fa-eraser"
aria-hidden="true"></i>
import { ToastModule } from 'ng2-toastr';
import { configureTestBed, i18nProviders } from '../../../../../testing/unit-test-helper';
+import { ConfigFormModel } from '../../../../shared/components/config-option/config-option.model';
import { SharedModule } from '../../../../shared/shared.module';
import { ConfigurationFormComponent } from './configuration-form.component';
-import { ConfigFormModel } from './configuration-form.model';
describe('ConfigurationFormComponent', () => {
let component: ConfigurationFormComponent;
expect(component.maxValue).toBe(5.2);
});
});
-
- describe('getStep', () => {
- it('should return the correct step for type uint and value 0', () => {
- const ret = component.getStep('uint', 0);
- expect(ret).toBe(1);
- });
-
- it('should return the correct step for type int and value 1', () => {
- const ret = component.getStep('int', 1);
- expect(ret).toBe(1);
- });
-
- it('should return the correct step for type int and value null', () => {
- const ret = component.getStep('int', null);
- expect(ret).toBe(1);
- });
-
- it('should return the correct step for type size and value 2', () => {
- const ret = component.getStep('size', 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 float and value 1', () => {
- const ret = component.getStep('float', 1);
- expect(ret).toBe(0.1);
- });
-
- it('should return the correct step for type float and value 0.1', () => {
- const ret = component.getStep('float', 0.1);
- expect(ret).toBe(0.1);
- });
-
- it('should return the correct step for type float and value 0.02', () => {
- const ret = component.getStep('float', 0.02);
- expect(ret).toBe(0.01);
- });
-
- it('should return the correct step for type float and value 0.003', () => {
- const ret = component.getStep('float', 0.003);
- expect(ret).toBe(0.001);
- });
-
- it('should return the correct step for type float and value null', () => {
- const ret = component.getStep('float', null);
- expect(ret).toBe(0.1);
- });
-
- it('should return undefined for unknown type', () => {
- const ret = component.getStep('unknown', 1);
- expect(ret).toBeUndefined();
- });
- });
});
import * as _ from 'lodash';
import { ConfigurationService } from '../../../../shared/api/configuration.service';
+import { ConfigFormModel } from '../../../../shared/components/config-option/config-option.model';
+import { ConfigOptionTypes } from '../../../../shared/components/config-option/config-option.types';
import { NotificationType } from '../../../../shared/enum/notification-type.enum';
import { CdFormGroup } from '../../../../shared/forms/cd-form-group';
import { NotificationService } from '../../../../shared/services/notification.service';
import { ConfigFormCreateRequestModel } from './configuration-form-create-request.model';
-import { ConfigFormModel } from './configuration-form.model';
-import { ConfigOptionTypes } from './configuration-form.types';
@Component({
selector: 'cd-configuration-form',
}
getStep(type: string, value: number): number | undefined {
- const numberTypes = ['uint', 'int', 'size', 'secs'];
-
- if (numberTypes.includes(type)) {
- return 1;
- }
-
- if (type === 'float') {
- if (value !== null) {
- const stringVal = value.toString();
- if (stringVal.indexOf('.') !== -1) {
- // Value type float and contains decimal characters
- const decimal = value.toString().split('.');
- return Math.pow(10, -decimal[1].length);
- }
- }
-
- return 0.1;
- }
-
- return undefined;
+ return ConfigOptionTypes.getTypeStep(type, value);
}
setResponse(response: ConfigFormModel) {
+++ /dev/null
-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>;
-}
+++ /dev/null
-import { ConfigFormModel } from './configuration-form.model';
-import { ConfigOptionTypes } from './configuration-form.types';
-
-describe('ConfigOptionTypes', () => {
- describe('getType', () => {
- it('should return uint type', () => {
- const ret = ConfigOptionTypes.getType('uint');
- expect(ret).toBeTruthy();
- expect(ret.name).toBe('uint');
- expect(ret.inputType).toBe('number');
- expect(ret.humanReadable).toBe('Unsigned integer value');
- expect(ret.defaultMin).toBe(0);
- expect(ret.patternHelpText).toBe('The entered value needs to be an unsigned number.');
- expect(ret.isNumberType).toBe(true);
- expect(ret.allowsNegative).toBe(false);
- });
-
- it('should return int type', () => {
- const ret = ConfigOptionTypes.getType('int');
- expect(ret).toBeTruthy();
- expect(ret.name).toBe('int');
- 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 type', () => {
- const ret = ConfigOptionTypes.getType('size');
- expect(ret).toBeTruthy();
- expect(ret.name).toBe('size');
- expect(ret.inputType).toBe('number');
- expect(ret.humanReadable).toBe('Unsigned integer value (>=16bit)');
- expect(ret.defaultMin).toBe(0);
- expect(ret.patternHelpText).toBe('The entered value needs to be a unsigned number.');
- expect(ret.isNumberType).toBe(true);
- expect(ret.allowsNegative).toBe(false);
- });
-
- it('should return secs type', () => {
- const ret = ConfigOptionTypes.getType('secs');
- expect(ret).toBeTruthy();
- expect(ret.name).toBe('secs');
- expect(ret.inputType).toBe('number');
- expect(ret.humanReadable).toBe('Number of seconds');
- expect(ret.defaultMin).toBe(1);
- expect(ret.patternHelpText).toBe('The entered value needs to be a number >= 1.');
- expect(ret.isNumberType).toBe(true);
- expect(ret.allowsNegative).toBe(false);
- });
-
- it('should return float type', () => {
- const ret = ConfigOptionTypes.getType('float');
- expect(ret).toBeTruthy();
- expect(ret.name).toBe('float');
- expect(ret.inputType).toBe('number');
- expect(ret.humanReadable).toBe('Double 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 str type', () => {
- const ret = ConfigOptionTypes.getType('str');
- expect(ret).toBeTruthy();
- expect(ret.name).toBe('str');
- 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 addr type', () => {
- const ret = ConfigOptionTypes.getType('addr');
- expect(ret).toBeTruthy();
- expect(ret.name).toBe('addr');
- 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 type', () => {
- const ret = ConfigOptionTypes.getType('uuid');
- expect(ret).toBeTruthy();
- expect(ret.name).toBe('uuid');
- 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 = ConfigOptionTypes.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(() => ConfigOptionTypes.getType('unknown')).toThrowError(
- 'Found unknown type "unknown" for config option.'
- );
- });
- });
-
- describe('getTypeValidators', () => {
- it('should return two validators for type uint, secs and size', () => {
- const types = ['uint', 'size', 'secs'];
-
- types.forEach((valType) => {
- const configOption = new ConfigFormModel();
- configOption.type = valType;
-
- const ret = ConfigOptionTypes.getTypeValidators(configOption);
- expect(ret).toBeTruthy();
- expect(ret.validators.length).toBe(2);
- });
- });
-
- it('should return a validator for types float, int, addr and uuid', () => {
- const types = ['float', 'int', 'addr', 'uuid'];
-
- types.forEach((valType) => {
- const configOption = new ConfigFormModel();
- configOption.type = valType;
-
- const ret = ConfigOptionTypes.getTypeValidators(configOption);
- expect(ret).toBeTruthy();
- expect(ret.validators.length).toBe(1);
- });
- });
-
- it('should return undefined for type bool and str', () => {
- const types = ['str', 'bool'];
-
- types.forEach((valType) => {
- const configOption = new ConfigFormModel();
- configOption.type = valType;
-
- const ret = ConfigOptionTypes.getTypeValidators(configOption);
- expect(ret).toBeUndefined();
- });
- });
-
- it('should return a pattern and a min validator', () => {
- const configOption = new ConfigFormModel();
- configOption.type = 'int';
- configOption.min = 2;
-
- const ret = ConfigOptionTypes.getTypeValidators(configOption);
- expect(ret).toBeTruthy();
- expect(ret.validators.length).toBe(2);
- expect(ret.min).toBe(2);
- expect(ret.max).toBeUndefined();
- });
-
- it('should return a pattern and a max validator', () => {
- const configOption = new ConfigFormModel();
- configOption.type = 'int';
- configOption.max = 5;
-
- const ret = ConfigOptionTypes.getTypeValidators(configOption);
- expect(ret).toBeTruthy();
- expect(ret.validators.length).toBe(2);
- expect(ret.min).toBeUndefined();
- expect(ret.max).toBe(5);
- });
-
- it('should return multiple validators', () => {
- const configOption = new ConfigFormModel();
- configOption.type = 'float';
- configOption.max = 5.2;
- configOption.min = 1.5;
-
- const ret = ConfigOptionTypes.getTypeValidators(configOption);
- expect(ret).toBeTruthy();
- expect(ret.validators.length).toBe(3);
- expect(ret.min).toBe(1.5);
- expect(ret.max).toBe(5.2);
- });
-
- it(
- 'should return a pattern help text for type uint, int, size, secs, ' + 'float, addr and uuid',
- () => {
- const types = ['uint', 'int', 'size', 'secs', 'float', 'addr', 'uuid'];
-
- types.forEach((valType) => {
- const configOption = new ConfigFormModel();
- configOption.type = valType;
-
- const ret = ConfigOptionTypes.getTypeValidators(configOption);
- expect(ret).toBeTruthy();
- expect(ret.patternHelpText).toBeDefined();
- });
- }
- );
- });
-});
+++ /dev/null
-import { Validators } from '@angular/forms';
-import { CdValidators } from '../../../../shared/forms/cd-validators';
-import { ConfigFormModel } from './configuration-form.model';
-
-import * as _ from 'lodash';
-
-export class ConfigOptionTypes {
- // TODO: I18N
- private static knownTypes: Array<any> = [
- {
- name: 'uint',
- inputType: 'number',
- humanReadable: 'Unsigned integer value',
- defaultMin: 0,
- patternHelpText: 'The entered value needs to be an unsigned number.',
- isNumberType: true,
- allowsNegative: false
- },
- {
- name: 'int',
- inputType: 'number',
- humanReadable: 'Integer value',
- patternHelpText: 'The entered value needs to be a number.',
- isNumberType: true,
- allowsNegative: true
- },
- {
- name: 'size',
- inputType: 'number',
- humanReadable: 'Unsigned integer value (>=16bit)',
- defaultMin: 0,
- patternHelpText: 'The entered value needs to be a unsigned number.',
- isNumberType: true,
- allowsNegative: false
- },
- {
- name: 'secs',
- inputType: 'number',
- humanReadable: 'Number of seconds',
- defaultMin: 1,
- patternHelpText: 'The entered value needs to be a number >= 1.',
- isNumberType: true,
- allowsNegative: false
- },
- {
- name: 'float',
- inputType: 'number',
- humanReadable: 'Double value',
- patternHelpText: 'The entered value needs to be a number or decimal.',
- isNumberType: true,
- allowsNegative: true
- },
- { name: 'str', inputType: 'text', humanReadable: 'Text', isNumberType: false },
- {
- name: 'addr',
- inputType: 'text',
- humanReadable: 'IPv4 or IPv6 address',
- patternHelpText: 'The entered value needs to be a valid IP address.',
- isNumberType: false
- },
- {
- name: 'uuid',
- 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 }
- ];
-
- public static getType(type: string): any {
- const currentType = _.find(this.knownTypes, (t) => {
- return t.name === type;
- });
-
- if (currentType !== undefined) {
- return currentType;
- }
-
- throw new Error('Found unknown type "' + type + '" for config option.');
- }
-
- public static getTypeValidators(configOption: ConfigFormModel): any {
- const typeParams = ConfigOptionTypes.getType(configOption.type);
-
- if (typeParams.name === 'bool' || typeParams.name === 'str') {
- return;
- }
-
- const typeValidators = { validators: [], patternHelpText: typeParams.patternHelpText };
-
- if (typeParams.isNumberType) {
- if (configOption.max && configOption.max !== '') {
- typeValidators['max'] = configOption.max;
- typeValidators.validators.push(Validators.max(configOption.max));
- }
-
- if (configOption.min && configOption.min !== '') {
- typeValidators['min'] = configOption.min;
- typeValidators.validators.push(Validators.min(configOption.min));
- } else if ('defaultMin' in typeParams) {
- typeValidators['min'] = typeParams.defaultMin;
- typeValidators.validators.push(Validators.min(typeParams.defaultMin));
- }
-
- if (configOption.type === 'float') {
- typeValidators.validators.push(CdValidators.decimalNumber());
- } else {
- typeValidators.validators.push(CdValidators.number(typeParams.allowsNegative));
- }
- } else if (configOption.type === 'addr') {
- typeValidators.validators = [CdValidators.ip()];
- } else if (configOption.type === 'uuid') {
- typeValidators.validators = [CdValidators.uuid()];
- }
-
- return typeValidators;
- }
-}
import { ConfigurationService } from '../../../../shared/api/configuration.service';
import { OsdService } from '../../../../shared/api/osd.service';
+import { ConfigOptionTypes } from '../../../../shared/components/config-option/config-option.types';
import { NotificationType } from '../../../../shared/enum/notification-type.enum';
import { CdFormGroup } from '../../../../shared/forms/cd-form-group';
import { NotificationService } from '../../../../shared/services/notification.service';
-import { ConfigOptionTypes } from '../../configuration/configuration-form/configuration-form.types';
@Component({
selector: 'cd-osd-recv-speed-modal',
import { DirectivesModule } from '../directives/directives.module';
import { PipesModule } from '../pipes/pipes.module';
import { BackButtonComponent } from './back-button/back-button.component';
+import { ConfigOptionComponent } from './config-option/config-option.component';
import { ConfirmationModalComponent } from './confirmation-modal/confirmation-modal.component';
import { CriticalConfirmationModalComponent } from './critical-confirmation-modal/critical-confirmation-modal.component';
import { ErrorPanelComponent } from './error-panel/error-panel.component';
GrafanaComponent,
SelectComponent,
BackButtonComponent,
- RefreshSelectorComponent
+ RefreshSelectorComponent,
+ ConfigOptionComponent
],
providers: [],
exports: [
LanguageSelectorComponent,
GrafanaComponent,
SelectComponent,
- RefreshSelectorComponent
+ RefreshSelectorComponent,
+ ConfigOptionComponent
],
entryComponents: [ModalComponent, CriticalConfirmationModalComponent, ConfirmationModalComponent]
})
--- /dev/null
+<div [formGroup]="optionsFormGroup">
+ <div *ngFor="let option of options; let last = last">
+ <div class="form-group"
+ [ngClass]="{'has-error': optionsForm.showError(option.name, optionsFormDir)}"
+ *ngIf="option.type === 'bool'">
+ <label class="col-sm-6 control-label"
+ [for]="option.name">
+ {{ option.text }}
+ <br>
+ <span class="text-muted">
+ {{ option.desc }}
+ <cd-helper *ngIf="option.long_desc">
+ {{ option.long_desc }}</cd-helper>
+ </span>
+ </label>
+ <div class="col-sm-6 checkbox-primary checkbox">
+ <input type="checkbox"
+ [id]="option.name"
+ [formControlName]="option.name">
+ <label></label>
+ </div>
+ </div>
+ <div class="form-group"
+ [ngClass]="{'has-error': optionsForm.showError(option.name, optionsFormDir)}"
+ *ngIf="option.type !== 'bool'">
+ <label class="col-sm-6 control-label"
+ [for]="option.name">{{ option.text }}
+ <br>
+ <span class="text-muted">
+ {{ option.desc }}
+ <cd-helper *ngIf="option.long_desc">
+ {{ option.long_desc }}</cd-helper>
+ </span>
+ </label>
+ <div class="col-sm-6">
+ <div class="input-group">
+ <input class="form-control"
+ [type]="option.additionalTypeInfo.inputType"
+ [id]="option.name"
+ [placeholder]="option.additionalTypeInfo.humanReadable"
+ [formControlName]="option.name"
+ [step]="getStep(option.type, optionsForm.getValue(option.name))">
+ <span class="input-group-btn"
+ *ngIf="optionsFormShowReset">
+ <button class="btn btn-default"
+ type="button"
+ data-toggle="button"
+ title="Remove the custom configuration value. The default configuration will be inherited and used instead."
+ (click)="resetValue(option.name)"
+ i18n-title>
+ <i class="fa fa-eraser"
+ aria-hidden="true"></i>
+ </button>
+ </span>
+ </div>
+ <span class="help-block"
+ *ngIf="optionsForm.showError(option.name, optionsFormDir, 'pattern')">
+ {{ option.additionalTypeInfo.patternHelpText }}</span>
+ <span class="help-block"
+ *ngIf="optionsForm.showError(option.name, optionsFormDir, 'invalidUuid')">
+ {{ option.additionalTypeInfo.patternHelpText }}</span>
+ <span class="help-block"
+ *ngIf="optionsForm.showError(option.name, optionsFormDir, 'max')"
+ i18n>The entered value is too high! It must not be greater than {{ option.maxValue }}.</span>
+ <span class="help-block"
+ *ngIf="optionsForm.showError(option.name, optionsFormDir, 'min')"
+ i18n>The entered value is too low! It must not be lower than {{ option.minValue }}.</span>
+ </div>
+ </div>
+ <hr *ngIf="!last">
+ </div>
+</div>
--- /dev/null
+hr {
+ margin-top: 5px;
+ margin-bottom: 5px;
+}
+
+.control-label {
+ text-align: left;
+}
+
+.checkbox-primary {
+ input {
+ width: 23px;
+ height: 15px;
+ margin-left: 0;
+ cursor: pointer;
+ }
+ label {
+ &:before,
+ &:after {
+ margin-left: 0;
+ }
+ cursor: auto;
+ }
+}
+
+.form-group {
+ .col-sm-6 {
+ padding-top: 7px;
+ }
+}
--- /dev/null
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+
+import * as _ from 'lodash';
+import { PopoverModule } from 'ngx-bootstrap/popover';
+import { of as observableOf } from 'rxjs';
+
+import { configureTestBed } from '../../../../testing/unit-test-helper';
+import { ConfigurationService } from '../../api/configuration.service';
+import { CdFormGroup } from '../../forms/cd-form-group';
+import { HelperComponent } from '../helper/helper.component';
+import { ConfigOptionComponent } from './config-option.component';
+
+describe('ConfigOptionComponent', () => {
+ let component: ConfigOptionComponent;
+ let fixture: ComponentFixture<ConfigOptionComponent>;
+ let configurationService: ConfigurationService;
+ let oNames: Array<string>;
+
+ configureTestBed({
+ declarations: [ConfigOptionComponent, HelperComponent],
+ imports: [PopoverModule.forRoot(), ReactiveFormsModule, HttpClientTestingModule],
+ providers: [ConfigurationService]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ConfigOptionComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ configurationService = TestBed.get(ConfigurationService);
+
+ const configOptions = [
+ {
+ name: 'osd_scrub_auto_repair_num_errors',
+ type: 'uint',
+ level: 'advanced',
+ desc: 'Maximum number of detected errors to automatically repair',
+ long_desc: '',
+ default: 5,
+ daemon_default: '',
+ tags: [],
+ services: [],
+ see_also: ['osd_scrub_auto_repair'],
+ min: '',
+ max: '',
+ can_update_at_runtime: true,
+ flags: []
+ },
+ {
+ name: 'osd_debug_deep_scrub_sleep',
+ type: 'float',
+ level: 'dev',
+ desc:
+ 'Inject an expensive sleep during deep scrub IO to make it easier to induce preemption',
+ long_desc: '',
+ default: 0,
+ daemon_default: '',
+ tags: [],
+ services: [],
+ see_also: [],
+ min: '',
+ max: '',
+ can_update_at_runtime: true,
+ flags: []
+ },
+ {
+ name: 'osd_heartbeat_interval',
+ type: 'int',
+ level: 'advanced',
+ desc: 'Interval (in seconds) between peer pings',
+ long_desc: '',
+ default: 6,
+ daemon_default: '',
+ tags: [],
+ services: [],
+ see_also: [],
+ min: 1,
+ max: 86400,
+ can_update_at_runtime: true,
+ flags: [],
+ value: [
+ {
+ section: 'osd',
+ value: 6
+ }
+ ]
+ },
+ {
+ name: 'bluestore_compression_algorithm',
+ type: 'str',
+ level: 'advanced',
+ desc: 'Default compression algorithm to use when writing object data',
+ long_desc:
+ 'This controls the default compressor to use (if any) if the ' +
+ 'per-pool property is not set. Note that zstd is *not* recommended for ' +
+ 'bluestore due to high CPU overhead when compressing small amounts of data.',
+ default: 'snappy',
+ daemon_default: '',
+ tags: [],
+ services: [],
+ see_also: [],
+ enum_values: ['', 'snappy', 'zlib', 'zstd', 'lz4'],
+ min: '',
+ max: '',
+ can_update_at_runtime: true,
+ flags: ['runtime']
+ },
+ {
+ name: 'rbd_discard_on_zeroed_write_same',
+ type: 'bool',
+ level: 'advanced',
+ desc: 'discard data on zeroed write same instead of writing zero',
+ long_desc: '',
+ default: true,
+ daemon_default: '',
+ tags: [],
+ services: ['rbd'],
+ see_also: [],
+ min: '',
+ max: '',
+ can_update_at_runtime: true,
+ flags: []
+ },
+ {
+ name: 'rbd_journal_max_payload_bytes',
+ type: 'size',
+ level: 'advanced',
+ desc: 'maximum journal payload size before splitting',
+ long_desc: '',
+ daemon_default: '',
+ tags: [],
+ services: ['rbd'],
+ see_also: [],
+ min: '',
+ max: '',
+ can_update_at_runtime: true,
+ flags: [],
+ default: '16384'
+ },
+ {
+ name: 'cluster_addr',
+ type: 'addr',
+ level: 'basic',
+ desc: 'cluster-facing address to bind to',
+ long_desc: '',
+ daemon_default: '',
+ tags: ['network'],
+ services: ['osd'],
+ see_also: [],
+ min: '',
+ max: '',
+ can_update_at_runtime: false,
+ flags: [],
+ default: '-'
+ },
+ {
+ name: 'fsid',
+ type: 'uuid',
+ level: 'basic',
+ desc: 'cluster fsid (uuid)',
+ long_desc: '',
+ daemon_default: '',
+ tags: ['service'],
+ services: ['common'],
+ see_also: [],
+ min: '',
+ max: '',
+ can_update_at_runtime: false,
+ flags: ['no_mon_update'],
+ default: '00000000-0000-0000-0000-000000000000'
+ },
+ {
+ name: 'mgr_tick_period',
+ type: 'secs',
+ level: 'advanced',
+ desc: 'Period in seconds of beacon messages to monitor',
+ long_desc: '',
+ daemon_default: '',
+ tags: [],
+ services: ['mgr'],
+ see_also: [],
+ min: '',
+ max: '',
+ can_update_at_runtime: true,
+ flags: [],
+ default: '2'
+ }
+ ];
+
+ spyOn(configurationService, 'filter').and.returnValue(observableOf(configOptions));
+ oNames = _.map(configOptions, 'name');
+ component.optionNames = oNames;
+ component.optionsForm = new CdFormGroup({});
+ component.optionsFormGroupName = 'testFormGroupName';
+ component.ngOnInit();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('optionNameToText', () => {
+ it('should format config option names correctly', () => {
+ const configOptionNames = {
+ osd_scrub_auto_repair_num_errors: 'Scrub Auto Repair Num Errors',
+ osd_debug_deep_scrub_sleep: 'Debug Deep Scrub Sleep',
+ osd_heartbeat_interval: 'Heartbeat Interval',
+ bluestore_compression_algorithm: 'Bluestore Compression Algorithm',
+ rbd_discard_on_zeroed_write_same: 'Rbd Discard On Zeroed Write Same',
+ rbd_journal_max_payload_bytes: 'Rbd Journal Max Payload Bytes',
+ cluster_addr: 'Cluster Addr',
+ fsid: 'Fsid',
+ mgr_tick_period: 'Tick Period'
+ };
+
+ component.options.forEach((option) => {
+ expect(option.text).toEqual(configOptionNames[option.name]);
+ });
+ });
+ });
+
+ describe('createForm', () => {
+ it('should set the optionsFormGroupName correctly', () => {
+ expect(component.optionsFormGroupName).toEqual('testFormGroupName');
+ });
+
+ it('should create a FormControl for every config option', () => {
+ component.options.forEach((option) => {
+ expect(Object.keys(component.optionsFormGroup.controls)).toContain(option.name);
+ });
+ });
+ });
+
+ describe('loadStorageData', () => {
+ it('should create a list of config options by names', () => {
+ expect(component.options.length).toEqual(9);
+
+ component.options.forEach((option) => {
+ expect(oNames).toContain(option.name);
+ });
+ });
+
+ it('should add all needed attributes to every config option', () => {
+ component.options.forEach((option) => {
+ const optionKeys = Object.keys(option);
+ expect(optionKeys).toContain('text');
+ expect(optionKeys).toContain('additionalTypeInfo');
+ expect(optionKeys).toContain('value');
+
+ if (option.type !== 'bool' && option.type !== 'str') {
+ expect(optionKeys).toContain('patternHelpText');
+ }
+
+ if (option.name === 'osd_heartbeat_interval') {
+ expect(optionKeys).toContain('maxValue');
+ expect(optionKeys).toContain('minValue');
+ }
+ });
+ });
+
+ it('should set minValue and maxValue correctly', () => {
+ component.options.forEach((option) => {
+ if (option.name === 'osd_heartbeat_interval') {
+ expect(option.minValue).toEqual(1);
+ expect(option.maxValue).toEqual(86400);
+ }
+ });
+ });
+
+ it('should set the value attribute correctly', () => {
+ component.options.forEach((option) => {
+ if (option.name === 'osd_heartbeat_interval') {
+ const value = option.value;
+ expect(value).toBeDefined();
+ expect(value).toEqual({ section: 'osd', value: 6 });
+ } else {
+ expect(option.value).toBeUndefined();
+ }
+ });
+ });
+
+ it('should set the FormControl value correctly', () => {
+ component.options.forEach((option) => {
+ const value = component.optionsFormGroup.getValue(option.name);
+ if (option.name === 'osd_heartbeat_interval') {
+ expect(value).toBeDefined();
+ expect(value).toEqual(6);
+ } else {
+ expect(value).toBeNull();
+ }
+ });
+ });
+ });
+});
--- /dev/null
+import { Component, Input, OnInit } from '@angular/core';
+import { FormControl, NgForm } from '@angular/forms';
+
+import * as _ from 'lodash';
+
+import { ConfigurationService } from '../../api/configuration.service';
+import { CdFormGroup } from '../../forms/cd-form-group';
+import { ConfigOptionTypes } from './config-option.types';
+
+@Component({
+ selector: 'cd-config-option',
+ templateUrl: './config-option.component.html',
+ styleUrls: ['./config-option.component.scss']
+})
+export class ConfigOptionComponent implements OnInit {
+ @Input()
+ optionNames: Array<string> = [];
+ @Input()
+ optionsForm: CdFormGroup = new CdFormGroup({});
+ @Input()
+ optionsFormDir: NgForm = new NgForm([], []);
+ @Input()
+ optionsFormGroupName = '';
+ @Input()
+ optionsFormShowReset = true;
+
+ options: Array<any> = [];
+ optionsFormGroup: CdFormGroup = new CdFormGroup({});
+
+ constructor(private configService: ConfigurationService) {}
+
+ private static optionNameToText(optionName: string): string {
+ const sections = ['mon', 'mgr', 'osd', 'mds', 'client'];
+ return optionName
+ .split('_')
+ .filter((c, index) => index !== 0 || !sections.includes(c))
+ .map((c) => c.charAt(0).toUpperCase() + c.substring(1))
+ .join(' ');
+ }
+
+ ngOnInit() {
+ this.createForm();
+ this.loadStoredData();
+ }
+
+ private createForm() {
+ this.optionsForm.addControl(this.optionsFormGroupName, this.optionsFormGroup);
+ this.optionNames.forEach((optionName) => {
+ this.optionsFormGroup.addControl(optionName, new FormControl(null));
+ });
+ }
+
+ getStep(type: string, value: any): number | undefined {
+ return ConfigOptionTypes.getTypeStep(type, value);
+ }
+
+ private loadStoredData() {
+ this.configService.filter(this.optionNames).subscribe((data: any) => {
+ this.options = data.map((configOption) => {
+ const formControl = this.optionsForm.get(configOption.name);
+ const typeValidators = ConfigOptionTypes.getTypeValidators(configOption);
+ configOption.additionalTypeInfo = ConfigOptionTypes.getType(configOption.type);
+
+ // Set general information and value
+ configOption.text = ConfigOptionComponent.optionNameToText(configOption.name);
+ configOption.value = _.find(configOption.value, (p) => {
+ return p.section === 'osd'; // TODO: Can handle any other section
+ });
+ if (configOption.value) {
+ if (configOption.additionalTypeInfo.name === 'bool') {
+ formControl.setValue(configOption.value.value === 'true');
+ } else {
+ formControl.setValue(configOption.value.value);
+ }
+ }
+
+ // Set type information and validators
+ if (typeValidators) {
+ configOption.patternHelpText = typeValidators.patternHelpText;
+ if ('max' in typeValidators && typeValidators.max !== '') {
+ configOption.maxValue = typeValidators.max;
+ }
+ if ('min' in typeValidators && typeValidators.min !== '') {
+ configOption.minValue = typeValidators.min;
+ }
+ formControl.setValidators(typeValidators.validators);
+ }
+
+ return configOption;
+ });
+ });
+ }
+
+ saveValues() {
+ const options = {};
+ this.optionNames.forEach((optionName) => {
+ const optionValue = this.optionsForm.getValue(optionName);
+ if (optionValue !== null && optionValue !== '') {
+ options[optionName] = {
+ section: 'osd', // TODO: Can handle any other section
+ value: optionValue
+ };
+ }
+ });
+
+ return this.configService.bulkCreate({ options: options });
+ }
+
+ resetValue(optionName: string) {
+ this.configService.delete(optionName, 'osd').subscribe(
+ // TODO: Can handle any other section
+ () => {
+ const formControl = this.optionsForm.get(optionName);
+ formControl.reset();
+ }
+ );
+ }
+}
--- /dev/null
+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>;
+}
--- /dev/null
+import { ConfigFormModel } from './config-option.model';
+import { ConfigOptionTypes } from './config-option.types';
+
+describe('ConfigOptionTypes', () => {
+ describe('getType', () => {
+ it('should return uint type', () => {
+ const ret = ConfigOptionTypes.getType('uint');
+ expect(ret).toBeTruthy();
+ expect(ret.name).toBe('uint');
+ expect(ret.inputType).toBe('number');
+ expect(ret.humanReadable).toBe('Unsigned integer value');
+ expect(ret.defaultMin).toBe(0);
+ expect(ret.patternHelpText).toBe('The entered value needs to be an unsigned number.');
+ expect(ret.isNumberType).toBe(true);
+ expect(ret.allowsNegative).toBe(false);
+ });
+
+ it('should return int type', () => {
+ const ret = ConfigOptionTypes.getType('int');
+ expect(ret).toBeTruthy();
+ expect(ret.name).toBe('int');
+ 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 type', () => {
+ const ret = ConfigOptionTypes.getType('size');
+ expect(ret).toBeTruthy();
+ expect(ret.name).toBe('size');
+ expect(ret.inputType).toBe('number');
+ expect(ret.humanReadable).toBe('Unsigned integer value (>=16bit)');
+ expect(ret.defaultMin).toBe(0);
+ expect(ret.patternHelpText).toBe('The entered value needs to be a unsigned number.');
+ expect(ret.isNumberType).toBe(true);
+ expect(ret.allowsNegative).toBe(false);
+ });
+
+ it('should return secs type', () => {
+ const ret = ConfigOptionTypes.getType('secs');
+ expect(ret).toBeTruthy();
+ expect(ret.name).toBe('secs');
+ expect(ret.inputType).toBe('number');
+ expect(ret.humanReadable).toBe('Number of seconds');
+ expect(ret.defaultMin).toBe(1);
+ expect(ret.patternHelpText).toBe('The entered value needs to be a number >= 1.');
+ expect(ret.isNumberType).toBe(true);
+ expect(ret.allowsNegative).toBe(false);
+ });
+
+ it('should return float type', () => {
+ const ret = ConfigOptionTypes.getType('float');
+ expect(ret).toBeTruthy();
+ expect(ret.name).toBe('float');
+ expect(ret.inputType).toBe('number');
+ expect(ret.humanReadable).toBe('Double 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 str type', () => {
+ const ret = ConfigOptionTypes.getType('str');
+ expect(ret).toBeTruthy();
+ expect(ret.name).toBe('str');
+ 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 addr type', () => {
+ const ret = ConfigOptionTypes.getType('addr');
+ expect(ret).toBeTruthy();
+ expect(ret.name).toBe('addr');
+ 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 type', () => {
+ const ret = ConfigOptionTypes.getType('uuid');
+ expect(ret).toBeTruthy();
+ expect(ret.name).toBe('uuid');
+ 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 = ConfigOptionTypes.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(() => ConfigOptionTypes.getType('unknown')).toThrowError(
+ 'Found unknown type "unknown" for config option.'
+ );
+ });
+ });
+
+ describe('getTypeValidators', () => {
+ it('should return two validators for type uint, secs and size', () => {
+ const types = ['uint', 'size', 'secs'];
+
+ types.forEach((valType) => {
+ const configOption = new ConfigFormModel();
+ configOption.type = valType;
+
+ const ret = ConfigOptionTypes.getTypeValidators(configOption);
+ expect(ret).toBeTruthy();
+ expect(ret.validators.length).toBe(2);
+ });
+ });
+
+ it('should return a validator for types float, int, addr and uuid', () => {
+ const types = ['float', 'int', 'addr', 'uuid'];
+
+ types.forEach((valType) => {
+ const configOption = new ConfigFormModel();
+ configOption.type = valType;
+
+ const ret = ConfigOptionTypes.getTypeValidators(configOption);
+ expect(ret).toBeTruthy();
+ expect(ret.validators.length).toBe(1);
+ });
+ });
+
+ it('should return undefined for type bool and str', () => {
+ const types = ['str', 'bool'];
+
+ types.forEach((valType) => {
+ const configOption = new ConfigFormModel();
+ configOption.type = valType;
+
+ const ret = ConfigOptionTypes.getTypeValidators(configOption);
+ expect(ret).toBeUndefined();
+ });
+ });
+
+ it('should return a pattern and a min validator', () => {
+ const configOption = new ConfigFormModel();
+ configOption.type = 'int';
+ configOption.min = 2;
+
+ const ret = ConfigOptionTypes.getTypeValidators(configOption);
+ expect(ret).toBeTruthy();
+ expect(ret.validators.length).toBe(2);
+ expect(ret.min).toBe(2);
+ expect(ret.max).toBeUndefined();
+ });
+
+ it('should return a pattern and a max validator', () => {
+ const configOption = new ConfigFormModel();
+ configOption.type = 'int';
+ configOption.max = 5;
+
+ const ret = ConfigOptionTypes.getTypeValidators(configOption);
+ expect(ret).toBeTruthy();
+ expect(ret.validators.length).toBe(2);
+ expect(ret.min).toBeUndefined();
+ expect(ret.max).toBe(5);
+ });
+
+ it('should return multiple validators', () => {
+ const configOption = new ConfigFormModel();
+ configOption.type = 'float';
+ configOption.max = 5.2;
+ configOption.min = 1.5;
+
+ const ret = ConfigOptionTypes.getTypeValidators(configOption);
+ expect(ret).toBeTruthy();
+ expect(ret.validators.length).toBe(3);
+ expect(ret.min).toBe(1.5);
+ expect(ret.max).toBe(5.2);
+ });
+
+ it(
+ 'should return a pattern help text for type uint, int, size, secs, ' + 'float, addr and uuid',
+ () => {
+ const types = ['uint', 'int', 'size', 'secs', 'float', 'addr', 'uuid'];
+
+ types.forEach((valType) => {
+ const configOption = new ConfigFormModel();
+ configOption.type = valType;
+
+ const ret = ConfigOptionTypes.getTypeValidators(configOption);
+ expect(ret).toBeTruthy();
+ expect(ret.patternHelpText).toBeDefined();
+ });
+ }
+ );
+ });
+
+ describe('getTypeStep', () => {
+ it('should return the correct step for type uint and value 0', () => {
+ const ret = ConfigOptionTypes.getTypeStep('uint', 0);
+ expect(ret).toBe(1);
+ });
+
+ it('should return the correct step for type int and value 1', () => {
+ const ret = ConfigOptionTypes.getTypeStep('int', 1);
+ expect(ret).toBe(1);
+ });
+
+ it('should return the correct step for type int and value null', () => {
+ const ret = ConfigOptionTypes.getTypeStep('int', null);
+ expect(ret).toBe(1);
+ });
+
+ it('should return the correct step for type size and value 2', () => {
+ const ret = ConfigOptionTypes.getTypeStep('size', 2);
+ expect(ret).toBe(1);
+ });
+
+ it('should return the correct step for type secs and value 3', () => {
+ const ret = ConfigOptionTypes.getTypeStep('secs', 3);
+ expect(ret).toBe(1);
+ });
+
+ it('should return the correct step for type float and value 1', () => {
+ const ret = ConfigOptionTypes.getTypeStep('float', 1);
+ expect(ret).toBe(0.1);
+ });
+
+ it('should return the correct step for type float and value 0.1', () => {
+ const ret = ConfigOptionTypes.getTypeStep('float', 0.1);
+ expect(ret).toBe(0.1);
+ });
+
+ it('should return the correct step for type float and value 0.02', () => {
+ const ret = ConfigOptionTypes.getTypeStep('float', 0.02);
+ expect(ret).toBe(0.01);
+ });
+
+ it('should return the correct step for type float and value 0.003', () => {
+ const ret = ConfigOptionTypes.getTypeStep('float', 0.003);
+ expect(ret).toBe(0.001);
+ });
+
+ it('should return the correct step for type float and value null', () => {
+ const ret = ConfigOptionTypes.getTypeStep('float', null);
+ expect(ret).toBe(0.1);
+ });
+
+ it('should return undefined for unknown type', () => {
+ const ret = ConfigOptionTypes.getTypeStep('unknown', 1);
+ expect(ret).toBeUndefined();
+ });
+ });
+});
--- /dev/null
+import { Validators } from '@angular/forms';
+
+import * as _ from 'lodash';
+
+import { CdValidators } from '../../forms/cd-validators';
+import { ConfigFormModel } from './config-option.model';
+
+export class ConfigOptionTypes {
+ // TODO: I18N
+ private static knownTypes: Array<any> = [
+ {
+ name: 'uint',
+ inputType: 'number',
+ humanReadable: 'Unsigned integer value',
+ defaultMin: 0,
+ patternHelpText: 'The entered value needs to be an unsigned number.',
+ isNumberType: true,
+ allowsNegative: false
+ },
+ {
+ name: 'int',
+ inputType: 'number',
+ humanReadable: 'Integer value',
+ patternHelpText: 'The entered value needs to be a number.',
+ isNumberType: true,
+ allowsNegative: true
+ },
+ {
+ name: 'size',
+ inputType: 'number',
+ humanReadable: 'Unsigned integer value (>=16bit)',
+ defaultMin: 0,
+ patternHelpText: 'The entered value needs to be a unsigned number.',
+ isNumberType: true,
+ allowsNegative: false
+ },
+ {
+ name: 'secs',
+ inputType: 'number',
+ humanReadable: 'Number of seconds',
+ defaultMin: 1,
+ patternHelpText: 'The entered value needs to be a number >= 1.',
+ isNumberType: true,
+ allowsNegative: false
+ },
+ {
+ name: 'float',
+ inputType: 'number',
+ humanReadable: 'Double value',
+ patternHelpText: 'The entered value needs to be a number or decimal.',
+ isNumberType: true,
+ allowsNegative: true
+ },
+ { name: 'str', inputType: 'text', humanReadable: 'Text', isNumberType: false },
+ {
+ name: 'addr',
+ inputType: 'text',
+ humanReadable: 'IPv4 or IPv6 address',
+ patternHelpText: 'The entered value needs to be a valid IP address.',
+ isNumberType: false
+ },
+ {
+ name: 'uuid',
+ 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 }
+ ];
+
+ public static getType(type: string): any {
+ const currentType = _.find(this.knownTypes, (t) => {
+ return t.name === type;
+ });
+
+ if (currentType !== undefined) {
+ return currentType;
+ }
+
+ throw new Error('Found unknown type "' + type + '" for config option.');
+ }
+
+ public static getTypeValidators(configOption: ConfigFormModel): any {
+ const typeParams = ConfigOptionTypes.getType(configOption.type);
+
+ if (typeParams.name === 'bool' || typeParams.name === 'str') {
+ return;
+ }
+
+ const typeValidators = { validators: [], patternHelpText: typeParams.patternHelpText };
+
+ if (typeParams.isNumberType) {
+ if (configOption.max && configOption.max !== '') {
+ typeValidators['max'] = configOption.max;
+ typeValidators.validators.push(Validators.max(configOption.max));
+ }
+
+ if (configOption.min && configOption.min !== '') {
+ typeValidators['min'] = configOption.min;
+ typeValidators.validators.push(Validators.min(configOption.min));
+ } else if ('defaultMin' in typeParams) {
+ typeValidators['min'] = typeParams.defaultMin;
+ typeValidators.validators.push(Validators.min(typeParams.defaultMin));
+ }
+
+ if (configOption.type === 'float') {
+ typeValidators.validators.push(CdValidators.decimalNumber());
+ } else {
+ typeValidators.validators.push(CdValidators.number(typeParams.allowsNegative));
+ }
+ } else if (configOption.type === 'addr') {
+ typeValidators.validators = [CdValidators.ip()];
+ } else if (configOption.type === 'uuid') {
+ typeValidators.validators = [CdValidators.uuid()];
+ }
+
+ return typeValidators;
+ }
+
+ public static getTypeStep(type: string, value: number): number | undefined {
+ const numberTypes = ['uint', 'int', 'size', 'secs'];
+
+ if (numberTypes.includes(type)) {
+ return 1;
+ }
+
+ if (type === 'float') {
+ if (value !== null) {
+ const stringVal = value.toString();
+ if (stringVal.indexOf('.') !== -1) {
+ // Value type float and contains decimal characters
+ const decimal = value.toString().split('.');
+ return Math.pow(10, -decimal[1].length);
+ }
+ }
+
+ return 0.1;
+ }
+
+ return undefined;
+ }
+}