From: Tatjana Dehler Date: Wed, 6 Mar 2019 15:42:28 +0000 (+0100) Subject: mgr/dashboard: Add config option component X-Git-Tag: v14.2.2~29^2~2 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=0c6cc0d4ec5776d8146fc35f099f2246ef79b8a0;p=ceph.git mgr/dashboard: Add config option component This commit adds an initial config option component in order to move the HTML template and the config option types related code to an own centralized place to be re-usable by other components Signed-off-by: Tatjana Dehler (cherry picked from commit 1834c68f843726107e57827754087fbd48bf9a13) --- diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-form/rbd-configuration-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-form/rbd-configuration-form.component.html index 63a5c88fc7c..7a1a59b1619 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-form/rbd-configuration-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-configuration-form/rbd-configuration-form.component.html @@ -57,10 +57,8 @@ 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)"> 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 index a80f9bd85a3..1c73c559c07 100644 --- 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 @@ -7,9 +7,9 @@ import { RouterTestingModule } from '@angular/router/testing'; 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; @@ -104,61 +104,4 @@ describe('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(); - }); - }); }); 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 index 11063864b65..0c05f1fc410 100644 --- 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 @@ -6,12 +6,12 @@ import { I18n } from '@ngx-translate/i18n-polyfill'; 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', @@ -84,26 +84,7 @@ export class ConfigurationFormComponent implements OnInit { } 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) { 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 deleted file mode 100644 index d3ebc5f37c6..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.model.ts +++ /dev/null @@ -1,12 +0,0 @@ -export class ConfigFormModel { - name: string; - desc: string; - long_desc: string; - type: string; - value: Array; - default: any; - daemon_default: any; - min: any; - max: any; - services: Array; -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.types.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.types.spec.ts deleted file mode 100644 index d5fa623af90..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.types.spec.ts +++ /dev/null @@ -1,215 +0,0 @@ -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(); - }); - } - ); - }); -}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.types.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.types.ts deleted file mode 100644 index ed8a3b7adc4..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/configuration/configuration-form/configuration-form.types.ts +++ /dev/null @@ -1,120 +0,0 @@ -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 = [ - { - 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; - } -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-recv-speed-modal/osd-recv-speed-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-recv-speed-modal/osd-recv-speed-modal.component.ts index dcb7db6985f..654fb619ea3 100755 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-recv-speed-modal/osd-recv-speed-modal.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-recv-speed-modal/osd-recv-speed-modal.component.ts @@ -9,10 +9,10 @@ import { mergeMap } from 'rxjs/operators'; 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', diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts index efc94139b23..e2ad026e218 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts @@ -12,6 +12,7 @@ import { TooltipModule } from 'ngx-bootstrap/tooltip'; 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'; @@ -63,7 +64,8 @@ import { WarningPanelComponent } from './warning-panel/warning-panel.component'; GrafanaComponent, SelectComponent, BackButtonComponent, - RefreshSelectorComponent + RefreshSelectorComponent, + ConfigOptionComponent ], providers: [], exports: [ @@ -82,7 +84,8 @@ import { WarningPanelComponent } from './warning-panel/warning-panel.component'; LanguageSelectorComponent, GrafanaComponent, SelectComponent, - RefreshSelectorComponent + RefreshSelectorComponent, + ConfigOptionComponent ], entryComponents: [ModalComponent, CriticalConfirmationModalComponent, ConfirmationModalComponent] }) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.component.html new file mode 100644 index 00000000000..0db04413d97 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.component.html @@ -0,0 +1,72 @@ +
+
+
+ +
+ + +
+
+
+ +
+
+ + + + +
+ + {{ option.additionalTypeInfo.patternHelpText }} + + {{ option.additionalTypeInfo.patternHelpText }} + The entered value is too high! It must not be greater than {{ option.maxValue }}. + The entered value is too low! It must not be lower than {{ option.minValue }}. +
+
+
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.component.scss new file mode 100644 index 00000000000..7c1347efb6f --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.component.scss @@ -0,0 +1,30 @@ +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; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.component.spec.ts new file mode 100644 index 00000000000..bc2d1ce3303 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.component.spec.ts @@ -0,0 +1,295 @@ +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; + let configurationService: ConfigurationService; + let oNames: Array; + + 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(); + } + }); + }); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.component.ts new file mode 100644 index 00000000000..e9d6a116c9c --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.component.ts @@ -0,0 +1,118 @@ +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 = []; + @Input() + optionsForm: CdFormGroup = new CdFormGroup({}); + @Input() + optionsFormDir: NgForm = new NgForm([], []); + @Input() + optionsFormGroupName = ''; + @Input() + optionsFormShowReset = true; + + options: Array = []; + 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(); + } + ); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.model.ts new file mode 100644 index 00000000000..d3ebc5f37c6 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.model.ts @@ -0,0 +1,12 @@ +export class ConfigFormModel { + name: string; + desc: string; + long_desc: string; + type: string; + value: Array; + default: any; + daemon_default: any; + min: any; + max: any; + services: Array; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.types.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.types.spec.ts new file mode 100644 index 00000000000..8c34111b957 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.types.spec.ts @@ -0,0 +1,272 @@ +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(); + }); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.types.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.types.ts new file mode 100644 index 00000000000..cfe1b4747f3 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/config-option/config-option.types.ts @@ -0,0 +1,144 @@ +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 = [ + { + 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; + } +}