From: Volker Theile Date: Tue, 22 Jan 2019 09:31:14 +0000 (+0100) Subject: mgr/dashboard: Configure all mgr modules in UI X-Git-Tag: v14.1.1~126^2~1 X-Git-Url: http://git.apps.os.sepia.ceph.com/?a=commitdiff_plain;h=c16d8f2964665c48d68609bb0b9e71e99fc2925b;p=ceph.git mgr/dashboard: Configure all mgr modules in UI Fixes: https://tracker.ceph.com/issues/37934 Signed-off-by: Volker Theile --- diff --git a/qa/tasks/mgr/dashboard/test_mgr_module.py b/qa/tasks/mgr/dashboard/test_mgr_module.py index 8348476fe41d3..9e7d760189ca5 100644 --- a/qa/tasks/mgr/dashboard/test_mgr_module.py +++ b/qa/tasks/mgr/dashboard/test_mgr_module.py @@ -3,9 +3,8 @@ from __future__ import absolute_import import logging import requests -import time -from .helper import DashboardTestCase, JObj, JList, JLeaf +from .helper import DashboardTestCase, JAny, JObj, JList, JLeaf logger = logging.getLogger(__name__) @@ -45,7 +44,24 @@ class MgrModuleTest(MgrModuleTestCase): JList( JObj(sub_elems={ 'name': JLeaf(str), - 'enabled': JLeaf(bool) + 'enabled': JLeaf(bool), + 'options': JObj( + {}, + allow_unknown=True, + unknown_schema=JObj({ + 'name': str, + 'type': str, + 'level': str, + 'flags': int, + 'default_value': JAny(none=False), + 'min': JAny(none=False), + 'max': JAny(none=False), + 'enum_allowed': JList(str), + 'see_also': JList(str), + 'desc': str, + 'long_desc': str, + 'tags': JList(str) + })) }))) module_info = self.find_object_in_list('name', 'telemetry', data) self.assertIsNotNone(module_info) @@ -61,7 +77,24 @@ class MgrModuleTest(MgrModuleTestCase): JList( JObj(sub_elems={ 'name': JLeaf(str), - 'enabled': JLeaf(bool) + 'enabled': JLeaf(bool), + 'options': JObj( + {}, + allow_unknown=True, + unknown_schema=JObj({ + 'name': str, + 'type': str, + 'level': str, + 'flags': int, + 'default_value': JAny(none=False), + 'min': JAny(none=False), + 'max': JAny(none=False), + 'enum_allowed': JList(str), + 'see_also': JList(str), + 'desc': str, + 'long_desc': str, + 'tags': JList(str) + })) }))) module_info = self.find_object_in_list('name', 'telemetry', data) self.assertIsNotNone(module_info) diff --git a/src/pybind/mgr/dashboard/controllers/mgr_modules.py b/src/pybind/mgr/dashboard/controllers/mgr_modules.py index 26eeebd9fc24f..358ae8be56abe 100644 --- a/src/pybind/mgr/dashboard/controllers/mgr_modules.py +++ b/src/pybind/mgr/dashboard/controllers/mgr_modules.py @@ -11,21 +11,26 @@ from ..tools import find_object_in_list, str_to_bool @ApiController('/mgr/module', Scope.CONFIG_OPT) class MgrModules(RESTController): - managed_modules = ['telemetry'] + ignore_modules = ['selftest'] def list(self): """ Get the list of managed modules. - :return: A list of objects with the fields 'name' and 'enabled'. + :return: A list of objects with the fields 'enabled', 'name' and 'options'. :rtype: list """ result = [] mgr_map = mgr.get('mgr_map') for module_config in mgr_map['available_modules']: - if self._is_module_managed(module_config['name']): - result.append({'name': module_config['name'], 'enabled': False}) + if module_config['name'] not in self.ignore_modules: + result.append({ + 'name': module_config['name'], + 'enabled': False, + 'options': self._convert_module_options( + module_config['module_options']) + }) for name in mgr_map['modules']: - if self._is_module_managed(name): + if name not in self.ignore_modules: obj = find_object_in_list('name', name, result) obj['enabled'] = True return result @@ -66,6 +71,8 @@ class MgrModules(RESTController): def enable(self, module_name): """ Enable the specified Ceph Mgr module. + :param module_name: The name of the Ceph Mgr module. + :type module_name: str """ assert self._is_module_managed(module_name) CephService.send_command( @@ -76,11 +83,25 @@ class MgrModules(RESTController): def disable(self, module_name): """ Disable the specified Ceph Mgr module. + :param module_name: The name of the Ceph Mgr module. + :type module_name: str """ assert self._is_module_managed(module_name) CephService.send_command( 'mon', 'mgr module disable', module=module_name) + @RESTController.Resource('GET') + def options(self, module_name): + """ + Get the module options of the specified Ceph Mgr module. + :param module_name: The name of the Ceph Mgr module. + :type module_name: str + :return: The module options as list of dicts. + :rtype: list + """ + assert self._is_module_managed(module_name) + return self._get_module_options(module_name) + def _is_module_managed(self, module_name): """ Check if the specified Ceph Mgr module is managed by this service. @@ -90,7 +111,13 @@ class MgrModules(RESTController): this service, otherwise ``false``. :rtype: bool """ - return module_name in self.managed_modules + if module_name in self.ignore_modules: + return False + mgr_map = mgr.get('mgr_map') + for module_config in mgr_map['available_modules']: + if module_name == module_config['name']: + return True + return False def _get_module_config(self, module_name): """ @@ -114,16 +141,28 @@ class MgrModules(RESTController): :rtype: dict """ options = self._get_module_config(module_name)['module_options'] - # Workaround a possible bug in the Ceph Mgr implementation. The - # 'default_value' field is always returned as a string. + return self._convert_module_options(options) + + def _convert_module_options(self, options): + # Workaround a possible bug in the Ceph Mgr implementation. + # Various fields (e.g. default_value, min, max) are always + # returned as a string. for option in options.values(): if option['type'] == 'str': - if option['default_value'] == 'None': + if option['default_value'] == 'None': # This is Python None option['default_value'] = '' elif option['type'] == 'bool': - option['default_value'] = str_to_bool(option['default_value']) + if option['default_value'] == '': + option['default_value'] = False + else: + option['default_value'] = str_to_bool( + option['default_value']) elif option['type'] == 'float': - option['default_value'] = float(option['default_value']) + for name in ['default_value', 'min', 'max']: + if option[name]: # Skip empty entries + option[name] = float(option[name]) elif option['type'] in ['uint', 'int', 'size', 'secs']: - option['default_value'] = int(option['default_value']) + for name in ['default_value', 'min', 'max']: + if option[name]: # Skip empty entries + option[name] = int(option[name]) return options diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts index c870c4cb25457..89b4e7dd1956d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts @@ -1,6 +1,8 @@ import { NgModule } from '@angular/core'; import { ActivatedRouteSnapshot, RouterModule, Routes } from '@angular/router'; +import * as _ from 'lodash'; + import { IscsiTargetFormComponent } from './ceph/block/iscsi-target-form/iscsi-target-form.component'; import { IscsiTargetListComponent } from './ceph/block/iscsi-target-list/iscsi-target-list.component'; import { IscsiComponent } from './ceph/block/iscsi/iscsi.component'; @@ -13,6 +15,8 @@ import { ConfigurationComponent } from './ceph/cluster/configuration/configurati import { CrushmapComponent } from './ceph/cluster/crushmap/crushmap.component'; import { HostsComponent } from './ceph/cluster/hosts/hosts.component'; import { LogsComponent } from './ceph/cluster/logs/logs.component'; +import { MgrModuleFormComponent } from './ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component'; +import { MgrModuleListComponent } from './ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component'; import { MonitorComponent } from './ceph/cluster/monitor/monitor.component'; import { OsdListComponent } from './ceph/cluster/osd/osd-list/osd-list.component'; import { PrometheusListComponent } from './ceph/cluster/prometheus/prometheus-list/prometheus-list.component'; @@ -36,8 +40,6 @@ import { SsoNotFoundComponent } from './core/auth/sso/sso-not-found/sso-not-foun import { UserFormComponent } from './core/auth/user-form/user-form.component'; import { UserListComponent } from './core/auth/user-list/user-list.component'; import { ForbiddenComponent } from './core/forbidden/forbidden.component'; -import { MgrModulesListComponent } from './core/mgr-modules/mgr-modules-list/mgr-modules-list.component'; -import { TelemetryComponent } from './core/mgr-modules/telemetry/telemetry.component'; import { NotFoundComponent } from './core/not-found/not-found.component'; import { BreadcrumbsResolver, IBreadcrumb } from './shared/models/breadcrumbs'; import { AuthGuardService } from './shared/services/auth-guard.service'; @@ -66,6 +68,14 @@ export class PerformanceCounterBreadcrumbsResolver extends BreadcrumbsResolver { } } +export class StartCaseBreadcrumbsResolver extends BreadcrumbsResolver { + resolve(route: ActivatedRouteSnapshot) { + const path = route.params.name; + const text = _.startCase(path); + return [{ text: text, path: path }]; + } +} + const routes: Routes = [ // Dashboard { path: '', redirectTo: 'dashboard', pathMatch: 'full' }, @@ -138,10 +148,19 @@ const routes: Routes = [ path: 'mgr-modules', canActivate: [AuthGuardService], canActivateChild: [AuthGuardService], - data: { breadcrumbs: 'Cluster/Manager Modules' }, + data: { breadcrumbs: 'Cluster/Manager modules' }, children: [ - { path: '', component: MgrModulesListComponent }, - { path: 'edit/telemetry', component: TelemetryComponent, data: { breadcrumbs: 'Telemetry' } } + { + path: '', + component: MgrModuleListComponent + }, + { + path: 'edit/:name', + component: MgrModuleFormComponent, + data: { + breadcrumbs: StartCaseBreadcrumbsResolver + } + } ] }, // Pools @@ -349,6 +368,6 @@ const routes: Routes = [ @NgModule({ imports: [RouterModule.forRoot(routes, { useHash: true })], exports: [RouterModule], - providers: [PerformanceCounterBreadcrumbsResolver] + providers: [StartCaseBreadcrumbsResolver, PerformanceCounterBreadcrumbsResolver] }) export class AppRoutingModule {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts index a8aeb7d855f7d..a84f0544c49e7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts @@ -19,6 +19,7 @@ import { CrushmapComponent } from './crushmap/crushmap.component'; import { HostDetailsComponent } from './hosts/host-details/host-details.component'; import { HostsComponent } from './hosts/hosts.component'; import { LogsComponent } from './logs/logs.component'; +import { MgrModulesModule } from './mgr-modules/mgr-modules.module'; import { MonitorComponent } from './monitor/monitor.component'; import { OsdDetailsComponent } from './osd/osd-details/osd-details.component'; import { OsdFlagsModalComponent } from './osd/osd-flags-modal/osd-flags-modal.component'; @@ -49,7 +50,8 @@ import { PrometheusListComponent } from './prometheus/prometheus-list/prometheus ModalModule.forRoot(), AlertModule.forRoot(), TooltipModule.forRoot(), - TreeModule + TreeModule, + MgrModulesModule ], declarations: [ HostsComponent, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-details/mgr-module-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-details/mgr-module-details.component.html new file mode 100644 index 0000000000000..f1a54892a600c --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-details/mgr-module-details.component.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-details/mgr-module-details.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-details/mgr-module-details.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-details/mgr-module-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-details/mgr-module-details.component.spec.ts new file mode 100644 index 0000000000000..236683404554a --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-details/mgr-module-details.component.spec.ts @@ -0,0 +1,31 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TabsModule } from 'ngx-bootstrap/tabs'; + +import { configureTestBed, i18nProviders } from '../../../../../testing/unit-test-helper'; +import { CdTableSelection } from '../../../../shared/models/cd-table-selection'; +import { SharedModule } from '../../../../shared/shared.module'; +import { MgrModuleDetailsComponent } from './mgr-module-details.component'; + +describe('MgrModuleDetailsComponent', () => { + let component: MgrModuleDetailsComponent; + let fixture: ComponentFixture; + + configureTestBed({ + declarations: [MgrModuleDetailsComponent], + imports: [HttpClientTestingModule, SharedModule, TabsModule.forRoot()], + providers: [i18nProviders] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(MgrModuleDetailsComponent); + component = fixture.componentInstance; + component.selection = new CdTableSelection(); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-details/mgr-module-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-details/mgr-module-details.component.ts new file mode 100644 index 0000000000000..dd166779d8af6 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-details/mgr-module-details.component.ts @@ -0,0 +1,27 @@ +import { Component, Input, OnChanges } from '@angular/core'; + +import { MgrModuleService } from '../../../../shared/api/mgr-module.service'; +import { CdTableSelection } from '../../../../shared/models/cd-table-selection'; + +@Component({ + selector: 'cd-mgr-module-details', + templateUrl: './mgr-module-details.component.html', + styleUrls: ['./mgr-module-details.component.scss'] +}) +export class MgrModuleDetailsComponent implements OnChanges { + module_config: any; + + @Input() + selection: CdTableSelection; + + constructor(private mgrModuleService: MgrModuleService) {} + + ngOnChanges() { + if (this.selection.hasSelection) { + const selectedItem = this.selection.first(); + this.mgrModuleService.getConfig(selectedItem.name).subscribe((resp: any) => { + this.module_config = resp; + }); + } + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.html new file mode 100644 index 0000000000000..1854e37f243b7 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.html @@ -0,0 +1,123 @@ +Loading configuration... +The configuration could not be loaded. + +
+
+
+
+

Edit Manager module

+
+
+
+ + + + + + +
+
+ + +
+
+ + +
+ + + The entered value is not a valid UUID, e.g.: 67dcac9f-2c03-4d6c-b7bd-1210b3a259a8 + The entered value needs to be a valid IP address. +
+ + +
+ + This field is required. + The entered value is too high! It must be lower or equal to {{ moduleOption.value.max }}. + The entered value is too low! It must be greater or equal to {{ moduleOption.value.min }}. + The entered value needs to be a number. +
+ + +
+ + This field is required. + The entered value needs to be a number or decimal. +
+ +
+
+ +
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.spec.ts new file mode 100644 index 0000000000000..1021482f84459 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.spec.ts @@ -0,0 +1,93 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { ToastModule } from 'ng2-toastr'; + +import { configureTestBed, i18nProviders } from '../../../../../testing/unit-test-helper'; +import { SharedModule } from '../../../../shared/shared.module'; +import { MgrModuleFormComponent } from './mgr-module-form.component'; + +describe('MgrModuleFormComponent', () => { + let component: MgrModuleFormComponent; + let fixture: ComponentFixture; + + configureTestBed({ + declarations: [MgrModuleFormComponent], + imports: [ + HttpClientTestingModule, + ReactiveFormsModule, + RouterTestingModule, + SharedModule, + ToastModule.forRoot() + ], + providers: i18nProviders + }); + + beforeEach(() => { + fixture = TestBed.createComponent(MgrModuleFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('getValidators', () => { + it('should return ip validator for type addr', () => { + const result = component.getValidators({ type: 'addr' }); + expect(result.length).toBe(1); + }); + + it('should return number, required validators for types uint, int, size, secs', () => { + const types = ['uint', 'int', 'size', 'secs']; + types.forEach((type) => { + const result = component.getValidators({ type: type }); + expect(result.length).toBe(2); + }); + }); + + it('should return number, required, min validators for types uint, int, size, secs', () => { + const types = ['uint', 'int', 'size', 'secs']; + types.forEach((type) => { + const result = component.getValidators({ type: type, min: 2 }); + expect(result.length).toBe(3); + }); + }); + + it('should return number, required, min, max validators for types uint, int, size, secs', () => { + const types = ['uint', 'int', 'size', 'secs']; + types.forEach((type) => { + const result = component.getValidators({ type: type, min: 2, max: 5 }); + expect(result.length).toBe(4); + }); + }); + + it('should return required, decimalNumber validators for type float', () => { + const result = component.getValidators({ type: 'float' }); + expect(result.length).toBe(2); + }); + + it('should return uuid validator for type uuid', () => { + const result = component.getValidators({ type: 'uuid' }); + expect(result.length).toBe(1); + }); + + it('should return no validator for type str', () => { + const result = component.getValidators({ type: 'str' }); + expect(result.length).toBe(0); + }); + + it('should return min validator for type str', () => { + const result = component.getValidators({ type: 'str', min: 1 }); + expect(result.length).toBe(1); + }); + + it('should return min, max validators for type str', () => { + const result = component.getValidators({ type: 'str', min: 1, max: 127 }); + expect(result.length).toBe(2); + }); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.ts new file mode 100644 index 0000000000000..ae525d5336554 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.ts @@ -0,0 +1,146 @@ +import { Component, OnInit } from '@angular/core'; +import { ValidatorFn, Validators } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { I18n } from '@ngx-translate/i18n-polyfill'; +import * as _ from 'lodash'; +import { forkJoin as observableForkJoin } from 'rxjs'; + +import { MgrModuleService } from '../../../../shared/api/mgr-module.service'; +import { NotificationType } from '../../../../shared/enum/notification-type.enum'; +import { CdFormBuilder } from '../../../../shared/forms/cd-form-builder'; +import { CdFormGroup } from '../../../../shared/forms/cd-form-group'; +import { CdValidators } from '../../../../shared/forms/cd-validators'; +import { NotificationService } from '../../../../shared/services/notification.service'; + +@Component({ + selector: 'cd-mgr-module-form', + templateUrl: './mgr-module-form.component.html', + styleUrls: ['./mgr-module-form.component.scss'] +}) +export class MgrModuleFormComponent implements OnInit { + mgrModuleForm: CdFormGroup; + error = false; + loading = false; + moduleName = ''; + moduleOptions = []; + + constructor( + private route: ActivatedRoute, + private router: Router, + private formBuilder: CdFormBuilder, + private mgrModuleService: MgrModuleService, + private notificationService: NotificationService, + private i18n: I18n + ) {} + + ngOnInit() { + this.route.params.subscribe( + (params: { name: string }) => { + this.moduleName = decodeURIComponent(params.name); + this.loading = true; + const observables = []; + observables.push(this.mgrModuleService.getOptions(this.moduleName)); + observables.push(this.mgrModuleService.getConfig(this.moduleName)); + observableForkJoin(observables).subscribe( + (resp: object) => { + this.loading = false; + this.moduleOptions = resp[0]; + // Create the form dynamically. + this.createForm(); + // Set the form field values. + this.mgrModuleForm.setValue(resp[1]); + }, + (error) => { + this.error = error; + } + ); + }, + (error) => { + this.error = error; + } + ); + } + + getValidators(moduleOption): ValidatorFn[] { + const result = []; + switch (moduleOption.type) { + case 'addr': + result.push(CdValidators.ip()); + break; + case 'uint': + case 'int': + case 'size': + case 'secs': + result.push(CdValidators.number()); + result.push(Validators.required); + if (_.isNumber(moduleOption.min)) { + result.push(Validators.min(moduleOption.min)); + } + if (_.isNumber(moduleOption.max)) { + result.push(Validators.max(moduleOption.max)); + } + break; + case 'str': + if (_.isNumber(moduleOption.min)) { + result.push(Validators.minLength(moduleOption.min)); + } + if (_.isNumber(moduleOption.max)) { + result.push(Validators.maxLength(moduleOption.max)); + } + break; + case 'float': + result.push(Validators.required); + result.push(CdValidators.decimalNumber()); + break; + case 'uuid': + result.push(CdValidators.uuid()); + break; + } + return result; + } + + createForm() { + const controlsConfig = {}; + _.forEach(this.moduleOptions, (moduleOption) => { + controlsConfig[moduleOption.name] = [ + moduleOption.default_value, + this.getValidators(moduleOption) + ]; + }); + this.mgrModuleForm = this.formBuilder.group(controlsConfig); + } + + goToListView() { + this.router.navigate(['/mgr-modules']); + } + + onSubmit() { + // Exit immediately if the form isn't dirty. + if (this.mgrModuleForm.pristine) { + this.goToListView(); + return; + } + const config = {}; + _.forEach(this.moduleOptions, (moduleOption) => { + const control = this.mgrModuleForm.get(moduleOption.name); + // Append the option only if the value has been modified. + if (control.dirty && control.valid) { + config[moduleOption.name] = control.value; + } + }); + this.mgrModuleService.updateConfig(this.moduleName, config).subscribe( + () => { + this.notificationService.show( + NotificationType.success, + this.i18n('Updated options for module "{{name}}".', { name: this.moduleName }) + ); + this.goToListView(); + }, + () => { + // Reset the 'Submit' button. + this.mgrModuleForm.setErrors({ cdSubmitButton: true }); + } + ); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.html new file mode 100644 index 0000000000000..967ae6612f7ef --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.html @@ -0,0 +1,18 @@ + + + + + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.spec.ts new file mode 100644 index 0000000000000..93b1498896925 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.spec.ts @@ -0,0 +1,153 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { ToastModule } from 'ng2-toastr'; +import { TabsModule } from 'ngx-bootstrap/tabs'; +import { of as observableOf, throwError as observableThrowError } from 'rxjs'; + +import { + configureTestBed, + i18nProviders, + PermissionHelper +} from '../../../../../testing/unit-test-helper'; +import { MgrModuleService } from '../../../../shared/api/mgr-module.service'; +import { TableActionsComponent } from '../../../../shared/datatable/table-actions/table-actions.component'; +import { CdTableSelection } from '../../../../shared/models/cd-table-selection'; +import { NotificationService } from '../../../../shared/services/notification.service'; +import { SharedModule } from '../../../../shared/shared.module'; +import { MgrModuleDetailsComponent } from '../mgr-module-details/mgr-module-details.component'; +import { MgrModuleListComponent } from './mgr-module-list.component'; + +describe('MgrModuleListComponent', () => { + let component: MgrModuleListComponent; + let fixture: ComponentFixture; + let mgrModuleService: MgrModuleService; + let notificationService: NotificationService; + + configureTestBed({ + declarations: [MgrModuleListComponent, MgrModuleDetailsComponent], + imports: [ + RouterTestingModule, + SharedModule, + HttpClientTestingModule, + TabsModule.forRoot(), + ToastModule.forRoot() + ], + providers: [MgrModuleService, NotificationService, i18nProviders] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(MgrModuleListComponent); + component = fixture.componentInstance; + mgrModuleService = TestBed.get(MgrModuleService); + notificationService = TestBed.get(NotificationService); + }); + + it('should create', () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + describe('show action buttons and drop down actions depending on permissions', () => { + let tableActions: TableActionsComponent; + let scenario: { fn; empty; single }; + let permissionHelper: PermissionHelper; + + const getTableActionComponent = (): TableActionsComponent => { + fixture.detectChanges(); + return fixture.debugElement.query(By.directive(TableActionsComponent)).componentInstance; + }; + + beforeEach(() => { + permissionHelper = new PermissionHelper(component.permission, () => + getTableActionComponent() + ); + scenario = { + fn: () => tableActions.getCurrentButton().name, + single: 'Edit', + empty: 'Edit' + }; + }); + + describe('with read and update', () => { + beforeEach(() => { + tableActions = permissionHelper.setPermissionsAndGetActions(0, 1, 0); + }); + + it('shows action button', () => permissionHelper.testScenarios(scenario)); + + it('shows all actions', () => { + expect(tableActions.tableActions.length).toBe(3); + expect(tableActions.tableActions).toEqual(component.tableActions); + }); + }); + + describe('with only read', () => { + beforeEach(() => { + tableActions = permissionHelper.setPermissionsAndGetActions(0, 0, 0); + }); + + it('shows no main action', () => { + permissionHelper.testScenarios({ + fn: () => tableActions.getCurrentButton(), + single: undefined, + empty: undefined + }); + }); + + it('shows no actions', () => { + expect(tableActions.tableActions.length).toBe(0); + expect(tableActions.tableActions).toEqual([]); + }); + }); + }); + + describe('should update module state', () => { + beforeEach(() => { + component.selection = new CdTableSelection(); + spyOn(notificationService, 'suspendToasties'); + spyOn(component.blockUI, 'start'); + spyOn(component.blockUI, 'stop'); + spyOn(component.table, 'refreshBtn'); + }); + + it('should enable module', fakeAsync(() => { + spyOn(mgrModuleService, 'enable').and.returnValue(observableThrowError('y')); + spyOn(mgrModuleService, 'list').and.returnValues(observableThrowError('z'), observableOf([])); + component.selection.selected.push({ + name: 'foo', + enabled: false + }); + component.selection.update(); + component.updateModuleState(); + tick(2000); + tick(2000); + expect(mgrModuleService.enable).toHaveBeenCalledWith('foo'); + expect(mgrModuleService.list).toHaveBeenCalledTimes(2); + expect(notificationService.suspendToasties).toHaveBeenCalledTimes(2); + expect(component.blockUI.start).toHaveBeenCalled(); + expect(component.blockUI.stop).toHaveBeenCalled(); + expect(component.table.refreshBtn).toHaveBeenCalled(); + })); + + it('should disable module', fakeAsync(() => { + spyOn(mgrModuleService, 'disable').and.returnValue(observableThrowError('x')); + spyOn(mgrModuleService, 'list').and.returnValue(observableOf([])); + component.selection.selected.push({ + name: 'bar', + enabled: true + }); + component.selection.update(); + component.updateModuleState(); + tick(2000); + expect(mgrModuleService.disable).toHaveBeenCalledWith('bar'); + expect(mgrModuleService.list).toHaveBeenCalledTimes(1); + expect(notificationService.suspendToasties).toHaveBeenCalledTimes(2); + expect(component.blockUI.start).toHaveBeenCalled(); + expect(component.blockUI.stop).toHaveBeenCalled(); + expect(component.table.refreshBtn).toHaveBeenCalled(); + })); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.ts new file mode 100644 index 0000000000000..50a13fd39ff27 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.ts @@ -0,0 +1,177 @@ +import { Component, ViewChild } from '@angular/core'; + +import { I18n } from '@ngx-translate/i18n-polyfill'; +import { BlockUI, NgBlockUI } from 'ng-block-ui'; +import { timer as observableTimer } from 'rxjs'; + +import { MgrModuleService } from '../../../../shared/api/mgr-module.service'; +import { TableComponent } from '../../../../shared/datatable/table/table.component'; +import { CellTemplate } from '../../../../shared/enum/cell-template.enum'; +import { CdTableAction } from '../../../../shared/models/cd-table-action'; +import { CdTableColumn } from '../../../../shared/models/cd-table-column'; +import { CdTableFetchDataContext } from '../../../../shared/models/cd-table-fetch-data-context'; +import { CdTableSelection } from '../../../../shared/models/cd-table-selection'; +import { Permission } from '../../../../shared/models/permissions'; +import { AuthStorageService } from '../../../../shared/services/auth-storage.service'; +import { NotificationService } from '../../../../shared/services/notification.service'; + +@Component({ + selector: 'cd-mgr-module-list', + templateUrl: './mgr-module-list.component.html', + styleUrls: ['./mgr-module-list.component.scss'] +}) +export class MgrModuleListComponent { + @ViewChild(TableComponent) + table: TableComponent; + @BlockUI() + blockUI: NgBlockUI; + + permission: Permission; + tableActions: CdTableAction[]; + columns: CdTableColumn[] = []; + modules: object[] = []; + selection: CdTableSelection = new CdTableSelection(); + + constructor( + private authStorageService: AuthStorageService, + private mgrModuleService: MgrModuleService, + private notificationService: NotificationService, + private i18n: I18n + ) { + this.permission = this.authStorageService.getPermissions().configOpt; + this.columns = [ + { + name: this.i18n('Name'), + prop: 'name', + flexGrow: 1 + }, + { + name: this.i18n('Enabled'), + prop: 'enabled', + flexGrow: 1, + cellClass: 'text-center', + cellTransformation: CellTemplate.checkIcon + } + ]; + const getModuleUri = () => + this.selection.first() && encodeURIComponent(this.selection.first().name); + this.tableActions = [ + { + name: this.i18n('Edit'), + permission: 'update', + disable: () => { + if (!this.selection.hasSelection) { + return true; + } + // Disable the 'edit' button when the module has no options. + return Object.values(this.selection.first().options).length === 0; + }, + routerLink: () => `/mgr-modules/edit/${getModuleUri()}`, + icon: 'fa-pencil' + }, + { + name: this.i18n('Enable'), + permission: 'update', + click: () => this.updateModuleState(), + disable: () => this.isTableActionDisabled('enabled'), + icon: 'fa-play' + }, + { + name: this.i18n('Disable'), + permission: 'update', + click: () => this.updateModuleState(), + disable: () => this.isTableActionDisabled('disabled'), + icon: 'fa-stop' + } + ]; + } + + getModuleList(context: CdTableFetchDataContext) { + this.mgrModuleService.list().subscribe( + (resp: object[]) => { + this.modules = resp; + }, + () => { + context.error(); + } + ); + } + + updateSelection(selection: CdTableSelection) { + this.selection = selection; + } + + /** + * Check if the table action is disabled. + * @param state The expected module state, e.g. ``enabled`` or ``disabled``. + * @returns If the specified state is validated to true or no selection is + * done, then ``true`` is returned, otherwise ``false``. + */ + isTableActionDisabled(state: 'enabled' | 'disabled') { + if (!this.selection.hasSelection) { + return true; + } + // Make sure the user can't modify the run state of the 'Dashboard' module. + // This check is only done in the UI because the REST API should still be + // able to do so. + if (this.selection.first().name === 'dashboard') { + return true; + } + switch (state) { + case 'enabled': + return this.selection.first().enabled; + case 'disabled': + return !this.selection.first().enabled; + } + } + + /** + * Update the Ceph Mgr module state to enabled or disabled. + */ + updateModuleState() { + if (!this.selection.hasSelection) { + return; + } + + let $obs; + const fnWaitUntilReconnected = () => { + observableTimer(2000).subscribe(() => { + // Trigger an API request to check if the connection is + // re-established. + this.mgrModuleService.list().subscribe( + () => { + // Resume showing the notification toasties. + this.notificationService.suspendToasties(false); + // Unblock the whole UI. + this.blockUI.stop(); + // Reload the data table content. + this.table.refreshBtn(); + }, + () => { + fnWaitUntilReconnected(); + } + ); + }); + }; + + // Note, the Ceph Mgr is always restarted when a module + // is enabled/disabled. + const module = this.selection.first(); + if (module.enabled) { + $obs = this.mgrModuleService.disable(module.name); + } else { + $obs = this.mgrModuleService.enable(module.name); + } + $obs.subscribe( + () => {}, + () => { + // Suspend showing the notification toasties. + this.notificationService.suspendToasties(true); + // Block the whole UI to prevent user interactions until + // the connection to the backend is reestablished + this.blockUI.start(this.i18n('Reconnecting, please wait ...')); + fnWaitUntilReconnected(); + } + ); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-modules.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-modules.module.ts new file mode 100644 index 0000000000000..1abf09a629cb4 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-modules.module.ts @@ -0,0 +1,23 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; + +import { TabsModule } from 'ngx-bootstrap/tabs'; + +import { AppRoutingModule } from '../../../app-routing.module'; +import { SharedModule } from '../../../shared/shared.module'; +import { MgrModuleDetailsComponent } from './mgr-module-details/mgr-module-details.component'; +import { MgrModuleFormComponent } from './mgr-module-form/mgr-module-form.component'; +import { MgrModuleListComponent } from './mgr-module-list/mgr-module-list.component'; + +@NgModule({ + imports: [ + AppRoutingModule, + CommonModule, + ReactiveFormsModule, + SharedModule, + TabsModule.forRoot() + ], + declarations: [MgrModuleListComponent, MgrModuleFormComponent, MgrModuleDetailsComponent] +}) +export class MgrModulesModule {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/core.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/core.module.ts index 5d61b6909a178..0190e52e22167 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/core.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/core.module.ts @@ -3,12 +3,11 @@ import { NgModule } from '@angular/core'; import { AuthModule } from './auth/auth.module'; import { ForbiddenComponent } from './forbidden/forbidden.component'; -import { MgrModulesModule } from './mgr-modules/mgr-modules.module'; import { NavigationModule } from './navigation/navigation.module'; import { NotFoundComponent } from './not-found/not-found.component'; @NgModule({ - imports: [CommonModule, NavigationModule, AuthModule, MgrModulesModule], + imports: [CommonModule, NavigationModule, AuthModule], exports: [NavigationModule], declarations: [NotFoundComponent, ForbiddenComponent] }) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/mgr-modules/mgr-modules-list/mgr-modules-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/mgr-modules/mgr-modules-list/mgr-modules-list.component.html deleted file mode 100644 index d0efeaef073c0..0000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/mgr-modules/mgr-modules-list/mgr-modules-list.component.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/mgr-modules/mgr-modules-list/mgr-modules-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/mgr-modules/mgr-modules-list/mgr-modules-list.component.scss deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/mgr-modules/mgr-modules-list/mgr-modules-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/mgr-modules/mgr-modules-list/mgr-modules-list.component.spec.ts deleted file mode 100644 index 3739b8f541ce1..0000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/mgr-modules/mgr-modules-list/mgr-modules-list.component.spec.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { ToastModule } from 'ng2-toastr'; - -import { - configureTestBed, - i18nProviders, - PermissionHelper -} from '../../../../testing/unit-test-helper'; -import { TableActionsComponent } from '../../../shared/datatable/table-actions/table-actions.component'; -import { SharedModule } from '../../../shared/shared.module'; -import { MgrModulesListComponent } from './mgr-modules-list.component'; - -describe('MgrModulesListComponent', () => { - let component: MgrModulesListComponent; - let fixture: ComponentFixture; - - configureTestBed({ - declarations: [MgrModulesListComponent], - imports: [RouterTestingModule, SharedModule, HttpClientTestingModule, ToastModule.forRoot()], - providers: i18nProviders - }); - - beforeEach(() => { - fixture = TestBed.createComponent(MgrModulesListComponent); - component = fixture.componentInstance; - }); - - it('should create', () => { - fixture.detectChanges(); - expect(component).toBeTruthy(); - }); - - describe('show action buttons and drop down actions depending on permissions', () => { - let tableActions: TableActionsComponent; - let scenario: { fn; empty; single }; - let permissionHelper: PermissionHelper; - - const getTableActionComponent = (): TableActionsComponent => { - fixture.detectChanges(); - return fixture.debugElement.query(By.directive(TableActionsComponent)).componentInstance; - }; - - beforeEach(() => { - permissionHelper = new PermissionHelper(component.permission, () => - getTableActionComponent() - ); - scenario = { - fn: () => tableActions.getCurrentButton().name, - single: 'Edit', - empty: 'Edit' - }; - }); - - describe('with read and update', () => { - beforeEach(() => { - tableActions = permissionHelper.setPermissionsAndGetActions(0, 1, 0); - }); - - it('shows action button', () => permissionHelper.testScenarios(scenario)); - - it('shows all actions', () => { - expect(tableActions.tableActions.length).toBe(3); - expect(tableActions.tableActions).toEqual(component.tableActions); - }); - }); - - describe('with only read', () => { - beforeEach(() => { - tableActions = permissionHelper.setPermissionsAndGetActions(0, 0, 0); - }); - - it('shows no main action', () => { - permissionHelper.testScenarios({ - fn: () => tableActions.getCurrentButton(), - single: undefined, - empty: undefined - }); - }); - - it('shows no actions', () => { - expect(tableActions.tableActions.length).toBe(0); - expect(tableActions.tableActions).toEqual([]); - }); - }); - }); -}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/mgr-modules/mgr-modules-list/mgr-modules-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/mgr-modules/mgr-modules-list/mgr-modules-list.component.ts deleted file mode 100644 index 94c7cf46021e7..0000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/mgr-modules/mgr-modules-list/mgr-modules-list.component.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { Component, ViewChild } from '@angular/core'; - -import { I18n } from '@ngx-translate/i18n-polyfill'; -import { BlockUI, NgBlockUI } from 'ng-block-ui'; -import { timer as observableTimer } from 'rxjs'; - -import { MgrModuleService } from '../../../shared/api/mgr-module.service'; -import { TableComponent } from '../../../shared/datatable/table/table.component'; -import { CellTemplate } from '../../../shared/enum/cell-template.enum'; -import { CdTableAction } from '../../../shared/models/cd-table-action'; -import { CdTableColumn } from '../../../shared/models/cd-table-column'; -import { CdTableFetchDataContext } from '../../../shared/models/cd-table-fetch-data-context'; -import { CdTableSelection } from '../../../shared/models/cd-table-selection'; -import { Permission } from '../../../shared/models/permissions'; -import { AuthStorageService } from '../../../shared/services/auth-storage.service'; -import { NotificationService } from '../../../shared/services/notification.service'; - -@Component({ - selector: 'cd-mgr-modules-list', - templateUrl: './mgr-modules-list.component.html', - styleUrls: ['./mgr-modules-list.component.scss'] -}) -export class MgrModulesListComponent { - @ViewChild(TableComponent) - table: TableComponent; - @BlockUI() - blockUI: NgBlockUI; - - permission: Permission; - tableActions: CdTableAction[]; - columns: CdTableColumn[] = []; - modules: object[] = []; - selection: CdTableSelection = new CdTableSelection(); - - constructor( - private authStorageService: AuthStorageService, - private mgrModuleService: MgrModuleService, - private notificationService: NotificationService, - private i18n: I18n - ) { - this.permission = this.authStorageService.getPermissions().configOpt; - this.columns = [ - { - name: this.i18n('Name'), - prop: 'name', - flexGrow: 1 - }, - { - name: this.i18n('Enabled'), - prop: 'enabled', - flexGrow: 1, - cellTransformation: CellTemplate.checkIcon - } - ]; - const getModuleUri = () => - this.selection.first() && encodeURIComponent(this.selection.first().name); - this.tableActions = [ - { - permission: 'update', - icon: 'fa-pencil', - routerLink: () => `/mgr-modules/edit/${getModuleUri()}`, - name: this.i18n('Edit') - }, - { - name: this.i18n('Enable'), - permission: 'update', - click: () => this.updateModuleState(), - disable: () => this.isTableActionDisabled('enabled'), - icon: 'fa-play' - }, - { - name: this.i18n('Disable'), - permission: 'update', - click: () => this.updateModuleState(), - disable: () => this.isTableActionDisabled('disabled'), - icon: 'fa-stop' - } - ]; - } - - getModuleList(context: CdTableFetchDataContext) { - this.mgrModuleService.list().subscribe( - (resp: object[]) => { - this.modules = resp; - }, - () => { - context.error(); - } - ); - } - - updateSelection(selection: CdTableSelection) { - this.selection = selection; - } - - /** - * Check if the table action is disabled. - * @param state The expected module state, e.g. ``enabled`` or ``disabled``. - * @returns If the specified state is validated to true or no selection is - * done, then ``true`` is returned, otherwise ``false``. - */ - isTableActionDisabled(state: 'enabled' | 'disabled') { - if (!this.selection.hasSelection) { - return true; - } - switch (state) { - case 'enabled': - return this.selection.first().enabled; - case 'disabled': - return !this.selection.first().enabled; - } - } - - /** - * Update the Ceph Mgr module state to enabled or disabled. - */ - updateModuleState() { - if (!this.selection.hasSelection) { - return; - } - - let $obs; - const fnWaitUntilReconnected = () => { - observableTimer(2000).subscribe(() => { - // Trigger an API request to check if the connection is - // re-established. - this.mgrModuleService.list().subscribe( - () => { - // Resume showing the notification toasties. - this.notificationService.suspendToasties(false); - // Unblock the whole UI. - this.blockUI.stop(); - // Reload the data table content. - this.table.refreshBtn(); - }, - () => { - fnWaitUntilReconnected(); - } - ); - }); - }; - - // Note, the Ceph Mgr is always restarted when a module - // is enabled/disabled. - const module = this.selection.first(); - if (module.enabled) { - $obs = this.mgrModuleService.disable(module.name); - } else { - $obs = this.mgrModuleService.enable(module.name); - } - $obs.subscribe( - () => {}, - () => { - // Suspend showing the notification toasties. - this.notificationService.suspendToasties(true); - // Block the whole UI to prevent user interactions until - // the connection to the backend is reestablished - this.blockUI.start(this.i18n('Reconnecting, please wait ...')); - fnWaitUntilReconnected(); - } - ); - } -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/mgr-modules/mgr-modules.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/mgr-modules/mgr-modules.module.ts deleted file mode 100644 index e101fef632559..0000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/mgr-modules/mgr-modules.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { ReactiveFormsModule } from '@angular/forms'; - -import { AppRoutingModule } from '../../app-routing.module'; -import { SharedModule } from '../../shared/shared.module'; -import { MgrModulesListComponent } from './mgr-modules-list/mgr-modules-list.component'; -import { TelemetryComponent } from './telemetry/telemetry.component'; - -@NgModule({ - imports: [CommonModule, ReactiveFormsModule, SharedModule, AppRoutingModule], - declarations: [TelemetryComponent, MgrModulesListComponent] -}) -export class MgrModulesModule {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/mgr-modules/telemetry/telemetry.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/mgr-modules/telemetry/telemetry.component.html deleted file mode 100644 index 58e4c076d4a88..0000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/mgr-modules/telemetry/telemetry.component.html +++ /dev/null @@ -1,144 +0,0 @@ -Loading configuration... -The configuration could not be loaded. - -
-
-
-
-

Telemetry

-
-
- - -
-
-
- - -
-
-
- - -
-
-
- - -
-
-
- - -
- -
- - This is not a valid email address. -
-
- - -
- -
- -
-
- - -
- -
- -
-
- - -
- -
- -
-
- - -
- -
- - This field is required. - The entered value must be at least 24 hours. - The entered value needs to be a number. -
-
- -
- -
-
-
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/mgr-modules/telemetry/telemetry.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/mgr-modules/telemetry/telemetry.component.scss deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/mgr-modules/telemetry/telemetry.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/mgr-modules/telemetry/telemetry.component.spec.ts deleted file mode 100644 index 63f7766e3e695..0000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/mgr-modules/telemetry/telemetry.component.spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ReactiveFormsModule } from '@angular/forms'; -import { RouterTestingModule } from '@angular/router/testing'; - -import { configureTestBed } from '../../../../testing/unit-test-helper'; -import { SharedModule } from '../../../shared/shared.module'; -import { TelemetryComponent } from './telemetry.component'; - -describe('TelemetryComponent', () => { - let component: TelemetryComponent; - let fixture: ComponentFixture; - - configureTestBed({ - declarations: [TelemetryComponent], - imports: [HttpClientTestingModule, ReactiveFormsModule, RouterTestingModule, SharedModule] - }); - - beforeEach(() => { - fixture = TestBed.createComponent(TelemetryComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/mgr-modules/telemetry/telemetry.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/mgr-modules/telemetry/telemetry.component.ts deleted file mode 100644 index 0245d2ddf4b68..0000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/mgr-modules/telemetry/telemetry.component.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { Validators } from '@angular/forms'; -import { Router } from '@angular/router'; - -import { MgrModuleService } from '../../../shared/api/mgr-module.service'; -import { CdFormBuilder } from '../../../shared/forms/cd-form-builder'; -import { CdFormGroup } from '../../../shared/forms/cd-form-group'; -import { CdValidators } from '../../../shared/forms/cd-validators'; - -@Component({ - selector: 'cd-telemetry', - templateUrl: './telemetry.component.html', - styleUrls: ['./telemetry.component.scss'] -}) -export class TelemetryComponent implements OnInit { - telemetryForm: CdFormGroup; - error = false; - loading = false; - - constructor( - private router: Router, - private formBuilder: CdFormBuilder, - private mgrModuleService: MgrModuleService - ) { - this.createForm(); - } - - createForm() { - this.telemetryForm = this.formBuilder.group({ - enabled: [false], - leaderboard: [false], - contact: [null, [CdValidators.email]], - organization: [null, [Validators.maxLength(256)]], - description: [null, [Validators.maxLength(256)]], - proxy: [null], - interval: [72, [Validators.min(24), CdValidators.number(), Validators.required]], - url: [null] - }); - } - - ngOnInit() { - this.loading = true; - this.mgrModuleService.getConfig('telemetry').subscribe( - (resp: object) => { - this.loading = false; - this.telemetryForm.setValue(resp); - }, - (error) => { - this.error = error; - } - ); - } - - goToListView() { - this.router.navigate(['/mgr-modules']); - } - - onSubmit() { - // Exit immediately if the form isn't dirty. - if (this.telemetryForm.pristine) { - this.goToListView(); - } - const config = {}; - const fieldNames = [ - 'enabled', - 'leaderboard', - 'contact', - 'organization', - 'description', - 'proxy', - 'interval' - ]; - fieldNames.forEach((fieldName) => { - config[fieldName] = this.telemetryForm.getValue(fieldName); - }); - this.mgrModuleService.updateConfig('telemetry', config).subscribe( - () => { - this.goToListView(); - }, - () => { - // Reset the 'Submit' button. - this.telemetryForm.setErrors({ cdSubmitButton: true }); - } - ); - } -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html index 60eeb1e6b1462..67b30c719f92c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html @@ -89,7 +89,7 @@ *ngIf="permissions.configOpt.read"> Manager Modules + routerLink="/mgr-modules">Manager modules
  • { const req = httpTesting.expectOne('api/mgr/module/bar/disable'); expect(req.request.method).toBe('POST'); }); + + it('should call getOptions', () => { + service.getOptions('foo').subscribe(); + const req = httpTesting.expectOne('api/mgr/module/foo/options'); + expect(req.request.method).toBe('GET'); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/mgr-module.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/mgr-module.service.ts index f5d68ca071c74..782ee737b76e6 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/mgr-module.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/mgr-module.service.ts @@ -1,6 +1,8 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + import { ApiModule } from './api.module'; @Injectable({ @@ -15,8 +17,8 @@ export class MgrModuleService { * Get the list of Ceph Mgr modules and their state (enabled/disabled). * @return {Observable} */ - list() { - return this.http.get(`${this.url}`); + list(): Observable { + return this.http.get(`${this.url}`); } /** @@ -24,7 +26,7 @@ export class MgrModuleService { * @param {string} module The name of the mgr module. * @return {Observable} */ - getConfig(module: string) { + getConfig(module: string): Observable { return this.http.get(`${this.url}/${module}`); } @@ -34,7 +36,7 @@ export class MgrModuleService { * @param {object} config The configuration. * @return {Observable} */ - updateConfig(module: string, config: Object) { + updateConfig(module: string, config: Object): Observable { return this.http.put(`${this.url}/${module}`, { config: config }); } @@ -53,4 +55,13 @@ export class MgrModuleService { disable(module: string) { return this.http.post(`${this.url}/${module}/disable`, null); } + + /** + * Get the Ceph Mgr module options. + * @param {string} module The name of the mgr module. + * @return {Observable} + */ + getOptions(module: string): Observable { + return this.http.get(`${this.url}/${module}/options`); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/locale/messages.xlf b/src/pybind/mgr/dashboard/frontend/src/locale/messages.xlf index 61978fea0a2ce..e51e45da4686f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/locale/messages.xlf +++ b/src/pybind/mgr/dashboard/frontend/src/locale/messages.xlf @@ -74,8 +74,8 @@ app/core/navigation/navigation/navigation.component.html 85 - - Manager Modules + + Manager modules app/core/navigation/navigation/navigation.component.html 92 @@ -254,6 +254,10 @@ app/ceph/cluster/configuration/configuration-form/configuration-form.component.html 159 + + app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.html + 118 + app/ceph/nfs/nfs-form/nfs-form.component.html 520 @@ -278,10 +282,6 @@ app/core/auth/user-form/user-form.component.html 151 - - app/core/mgr-modules/telemetry/telemetry.component.html - 139 - Select a Language @@ -515,6 +515,14 @@ app/ceph/block/rbd-form/rbd-form.component.html 181 + + app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.html + 79 + + + app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.html + 100 + app/ceph/cluster/osd/osd-recv-speed-modal/osd-recv-speed-modal.component.html 31 @@ -615,10 +623,6 @@ app/core/auth/user-form/user-form.component.html 87 - - app/core/mgr-modules/telemetry/telemetry.component.html - 118 - app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.html 36 @@ -1288,10 +1292,6 @@ app/core/auth/role-form/role-form.component.html 46 - - app/core/mgr-modules/telemetry/telemetry.component.html - 80 - app/ceph/cluster/configuration/configuration-details/configuration-details.component.html 13 @@ -1394,6 +1394,70 @@ app/ceph/cluster/logs/logs.component.html 20 + + Loading configuration... + + app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.html + 2 + + + The configuration could not be loaded. + + app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.html + 4 + + + Edit Manager module + + app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.html + 15 + + + The entered value is not a valid UUID, e.g.: 67dcac9f-2c03-4d6c-b7bd-1210b3a259a8 + + app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.html + 62 + + + The entered value needs to be a valid IP address. + + app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.html + 65 + + + The entered value is too high! It must be lower or equal to . + + app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.html + 82 + + + The entered value is too low! It must be greater or equal to . + + app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.html + 85 + + + The entered value needs to be a number. + + app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.html + 88 + + + The entered value needs to be a number or decimal. + + app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.html + 103 + + + Update + + app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.html + 113 + + + app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.html + 41 + Status @@ -1645,6 +1709,10 @@ app/ceph/cluster/configuration/configuration-details/configuration-details.component.html 3 + + app/ceph/cluster/mgr-modules/mgr-module-details/mgr-module-details.component.html + 2 + app/ceph/block/rbd-details/rbd-details.component.html 8 @@ -2822,10 +2890,6 @@ app/ceph/rgw/rgw-user-form/rgw-user-form.component.html 79 - - app/core/mgr-modules/telemetry/telemetry.component.html - 58 - The chosen email address is already in use. @@ -2992,10 +3056,6 @@ app/ceph/rgw/rgw-user-form/rgw-user-form.component.html 506 - - app/core/mgr-modules/telemetry/telemetry.component.html - 27 - app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.html 71 @@ -3219,76 +3279,6 @@ app/core/forbidden/forbidden.component.html 7 - - Loading configuration... - - app/core/mgr-modules/telemetry/telemetry.component.html - 2 - - - The configuration could not be loaded. - - app/core/mgr-modules/telemetry/telemetry.component.html - 4 - - - Telemetry - - app/core/mgr-modules/telemetry/telemetry.component.html - 15 - - - Leaderboard - - app/core/mgr-modules/telemetry/telemetry.component.html - 40 - - - Contact - - app/core/mgr-modules/telemetry/telemetry.component.html - 50 - - - Organization - - app/core/mgr-modules/telemetry/telemetry.component.html - 66 - - - Proxy - - app/core/mgr-modules/telemetry/telemetry.component.html - 94 - - - Interval - - app/core/mgr-modules/telemetry/telemetry.component.html - 109 - - - The entered value must be at least 24 hours. - - app/core/mgr-modules/telemetry/telemetry.component.html - 121 - - - The entered value needs to be a number. - - app/core/mgr-modules/telemetry/telemetry.component.html - 124 - - - Update - - app/core/mgr-modules/telemetry/telemetry.component.html - 134 - - - app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.html - 41 - Sorry, we could not find what you were looking for @@ -4843,6 +4833,34 @@ 1 + + Updated options for module "". + + src/app/ceph/cluster/mgr-modules/mgr-module-form/mgr-module-form.component.ts + 1 + + + + Enable + + src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.ts + 1 + + + + Disable + + src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.ts + 1 + + + + Reconnecting, please wait ... + + src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.ts + 1 + + Public Address @@ -5771,27 +5789,6 @@ 1 - - Enable - - src/app/core/mgr-modules/mgr-modules-list/mgr-modules-list.component.ts - 1 - - - - Disable - - src/app/core/mgr-modules/mgr-modules-list/mgr-modules-list.component.ts - 1 - - - - Reconnecting, please wait ... - - src/app/core/mgr-modules/mgr-modules-list/mgr-modules-list.component.ts - 1 - - Each object is split in data-chunks parts, each stored on a different OSD. diff --git a/src/pybind/mgr/telemetry/module.py b/src/pybind/mgr/telemetry/module.py index 283f5ee3cf45c..9d54298eadd90 100644 --- a/src/pybind/mgr/telemetry/module.py +++ b/src/pybind/mgr/telemetry/module.py @@ -70,7 +70,8 @@ class Module(MgrModule): { 'name': 'interval', 'type': 'int', - 'default': 72 + 'default': 72, + 'min': 24 } ]