From 12f78b48b0f905331bfbc53b5f279bd1bd7fc0cb Mon Sep 17 00:00:00 2001 From: Pedro Gonzalez Gomez Date: Mon, 20 Feb 2023 14:37:00 +0100 Subject: [PATCH] mgr/dashboard: Edit ceph authx users Signed-off-by: Pedro Gonzalez Gomez Signed-off-by: Pere Diaz Bou (cherry picked from commit 8177a748bd831568417df5c687109fbbbd9b981d) (cherry picked from commit bc73c1aec686282547d6b920ef7ef239d0231f40) --- src/pybind/mgr/dashboard/controllers/_crud.py | 45 ++++++++--- .../mgr/dashboard/controllers/ceph_users.py | 78 +++++++++++++++---- .../frontend/src/app/app-routing.module.ts | 8 ++ .../submit-button/submit-button.component.ts | 2 +- .../crud-table/crud-table.component.ts | 13 +++- .../forms/crud-form/crud-form.component.ts | 41 +++++++--- .../shared/forms/crud-form/crud-form.model.ts | 2 + .../crud-form-adapter.service.spec.ts | 5 +- .../services/crud-form-adapter.service.ts | 5 +- .../services/data-gateway.service.spec.ts | 3 +- .../shared/services/data-gateway.service.ts | 32 ++++++-- .../shared/services/task-message.service.ts | 8 +- src/pybind/mgr/dashboard/openapi.yaml | 66 +++++++++++++++- 13 files changed, 256 insertions(+), 52 deletions(-) diff --git a/src/pybind/mgr/dashboard/controllers/_crud.py b/src/pybind/mgr/dashboard/controllers/_crud.py index 7b2f13defac3c..20152e900da30 100644 --- a/src/pybind/mgr/dashboard/controllers/_crud.py +++ b/src/pybind/mgr/dashboard/controllers/_crud.py @@ -91,6 +91,7 @@ class Icon(Enum): DESTROY = 'fa fa-times' IMPORT = 'fa fa-upload' EXPORT = 'fa fa-download' + EDIT = 'fa fa-pencil' class Validator(Enum): @@ -102,7 +103,7 @@ class Validator(Enum): class FormField(NamedTuple): """ - The key of a FromField is then used to send the data related to that key into the + The key of a FormField is then used to send the data related to that key into the POST and PUT endpoints. It is imperative for the developer to map keys of fields and containers to the input of the POST and PUT endpoints. """ @@ -111,6 +112,7 @@ class FormField(NamedTuple): field_type: Any = str default_value: Optional[Any] = None optional: bool = False + readonly: bool = False help: str = '' validators: List[Validator] = [] @@ -133,11 +135,12 @@ class FormField(NamedTuple): class Container: def __init__(self, name: str, key: str, fields: List[Union[FormField, "Container"]], - optional: bool = False, min_items=1): + optional: bool = False, readonly: bool = False, min_items=1): self.name = name self.key = key self.fields = fields self.optional = optional + self.readonly = readonly self.min_items = min_items def layout_type(self): @@ -215,6 +218,7 @@ class Container: properties[field.key]['type'] = _type properties[field.key]['title'] = field.name field_ui_schema['key'] = field_key + field_ui_schema['readonly'] = field.readonly field_ui_schema['help'] = f'{field.help}' field_ui_schema['validators'] = [i.value for i in field.validators] items.append(field_ui_schema) @@ -272,16 +276,21 @@ class FormTaskInfo: class Form: - def __init__(self, path, root_container, - task_info: FormTaskInfo = FormTaskInfo("Unknown task", [])): + def __init__(self, path, root_container, method_type='', + task_info: FormTaskInfo = FormTaskInfo("Unknown task", []), + model_callback=None): self.path = path self.root_container: Container = root_container + self.method_type = method_type self.task_info = task_info + self.model_callback = model_callback def to_dict(self): res = self.root_container.to_dict() + res['method_type'] = self.method_type res['task_info'] = self.task_info.to_dict() res['path'] = self.path + res['ask'] = self.path return res @@ -319,9 +328,10 @@ class CRUDEndpoint: meta: CRUDMeta = CRUDMeta(), get_all: Optional[CRUDCollectionMethod] = None, create: Optional[CRUDCollectionMethod] = None, delete: Optional[CRUDCollectionMethod] = None, - detail_columns: Optional[List[str]] = None, selection_type: SelectionType = SelectionType.SINGLE, - extra_endpoints: Optional[List[Tuple[str, CRUDCollectionMethod]]] = None): + extra_endpoints: Optional[List[Tuple[str, CRUDCollectionMethod]]] = None, + edit: Optional[CRUDCollectionMethod] = None, + detail_columns: Optional[List[str]] = None): self.router = router self.doc = doc self.set_column = set_column @@ -331,6 +341,7 @@ class CRUDEndpoint: self.get_all = get_all self.create = create self.delete = delete + self.edit = edit self.permissions = permissions if permissions is not None else [] self.column_key = column_key if column_key is not None else '' self.detail_columns = detail_columns if detail_columns is not None else [] @@ -372,6 +383,13 @@ class CRUDEndpoint: return outer_self.delete.func(self, *args, **kwargs) # type: ignore funcs['delete'] = delete + if self.edit: + @self.edit.doc + @wraps(self.edit.func) + def singleton_set(self, *args, **kwargs): + return outer_self.edit.func(self, *args, **kwargs) # type: ignore + funcs['singleton_set'] = singleton_set + for extra_endpoint in self.extra_endpoints: funcs[extra_endpoint[0]] = extra_endpoint[1].doc(extra_endpoint[1].func) @@ -386,10 +404,10 @@ class CRUDEndpoint: cls.CRUDClass = crud_class def create_meta_class(self, cls): - def _list(self): + def _list(self, model_key: str = ''): self.update_columns() self.generate_actions() - self.generate_forms() + self.generate_forms(model_key) self.set_permissions() self.set_column_key() self.get_detail_columns() @@ -424,13 +442,20 @@ class CRUDEndpoint: for action in self.__class__.outer_self.actions: self.__class__.outer_self.meta.actions.append(action._asdict()) - def generate_forms(self): + def generate_forms(self, model_key): self.__class__.outer_self.meta.forms.clear() for form in self.__class__.outer_self.forms: - self.__class__.outer_self.meta.forms.append(form.to_dict()) + form_as_dict = form.to_dict() + model = {} + if form.model_callback and model_key: + model = form.model_callback(model_key) + form_as_dict['model'] = model + self.__class__.outer_self.meta.forms.append(form_as_dict) def set_permissions(self): + self.__class__.outer_self.meta.permissions.clear() + if self.__class__.outer_self.permissions: self.outer_self.meta.permissions.extend(self.__class__.outer_self.permissions) diff --git a/src/pybind/mgr/dashboard/controllers/ceph_users.py b/src/pybind/mgr/dashboard/controllers/ceph_users.py index 77e67dc7ff840..a4593144739df 100644 --- a/src/pybind/mgr/dashboard/controllers/ceph_users.py +++ b/src/pybind/mgr/dashboard/controllers/ceph_users.py @@ -1,4 +1,5 @@ import logging +from enum import Enum from errno import EINVAL from typing import List, NamedTuple, Optional @@ -26,6 +27,11 @@ class Cap(NamedTuple): cap: str +class MethodType(Enum): + POST = 'post' + PUT = 'put' + + class CephUserEndpoints: @staticmethod def _run_auth_command(command: str, *args, **kwargs): @@ -75,16 +81,10 @@ class CephUserEndpoints: def user_delete(_, user_entity: str): """ Delete a ceph user and it's defined capabilities. - :param user_entity: Entity to dlelete + :param user_entity: Entity to delete """ logger.debug("Sending command 'auth del' of entity '%s'", user_entity) - try: - CephUserEndpoints._run_auth_command('auth del', entity=user_entity) - except SendCommandError as ex: - msg = f'{ex} in command {ex.prefix}' - if ex.errno == -EINVAL: - raise DashboardException(msg, code=400) - raise DashboardException(msg, code=500) + CephUserEndpoints._run_auth_command('auth del', entity=user_entity) return f"Successfully deleted user '{user_entity}'" @staticmethod @@ -95,8 +95,35 @@ class CephUserEndpoints: export_string += f'{out}\n' return export_string + @staticmethod + def user_edit(_, user_entity: str = '', capabilities: List[Cap] = None): + """ + Change the ceph user capabilities. + Setting new capabilities will overwrite current ones. + :param user_entity: Entity to change + :param capabilities: List of updated capabilities to user_entity + """ + caps = [] + for cap in capabilities: + caps.append(cap['entity']) + caps.append(cap['cap']) + + logger.debug("Sending command 'auth caps' of entity '%s' with caps '%s'", + user_entity, str(caps)) + CephUserEndpoints._run_auth_command('auth caps', entity=user_entity, caps=caps) + return f"Successfully edited user '{user_entity}'" + + @staticmethod + def model(user_entity: str): + user_data = CephUserEndpoints._run_auth_command('auth get', entity=user_entity)[0] + model = {'user_entity': '', 'capabilities': []} + model['user_entity'] = user_data['entity'] + for entity, cap in user_data['caps'].items(): + model['capabilities'].append({'entity': entity, 'cap': cap}) + return model + -create_cap_container = ArrayHorizontalContainer('Capabilities', 'capabilities', fields=[ +cap_container = ArrayHorizontalContainer('Capabilities', 'capabilities', fields=[ FormField('Entity', 'entity', field_type=str), FormField('Entity Capabilities', @@ -105,12 +132,19 @@ create_cap_container = ArrayHorizontalContainer('Capabilities', 'capabilities', create_container = VerticalContainer('Create User', 'create_user', fields=[ FormField('User entity', 'user_entity', field_type=str), - create_cap_container, + cap_container, +]) + +edit_container = VerticalContainer('Edit User', 'edit_user', fields=[ + FormField('User entity', 'user_entity', + field_type=str, readonly=True), + cap_container, ]) create_form = Form(path='/cluster/user/create', root_container=create_container, - task_info=FormTaskInfo("Ceph user '{user_entity}' created successfully", + method_type=MethodType.POST.value, + task_info=FormTaskInfo("Ceph user '{user_entity}' successfully", ['user_entity'])) # pylint: disable=C0301 @@ -127,7 +161,15 @@ import_container = VerticalContainer('Import User', 'import_user', fields=[ import_user_form = Form(path='/cluster/user/import', root_container=import_container, - task_info=FormTaskInfo("User imported successfully", [])) + task_info=FormTaskInfo("successfully", []), + method_type=MethodType.POST.value) + +edit_form = Form(path='/cluster/user/edit', + root_container=edit_container, + method_type=MethodType.PUT.value, + task_info=FormTaskInfo("Ceph user '{user_entity}' successfully", + ['user_entity']), + model_callback=CephUserEndpoints.model) @CRUDEndpoint( @@ -137,16 +179,18 @@ import_user_form = Form(path='/cluster/user/import', actions=[ TableAction(name='Create', permission='create', icon=Icon.ADD.value, routerLink='/cluster/user/create'), + TableAction(name='Edit', permission='update', icon=Icon.EDIT.value, + click='edit'), TableAction(name='Delete', permission='delete', icon=Icon.DESTROY.value, click='delete', disable=True), TableAction(name='Import', permission='create', icon=Icon.IMPORT.value, routerLink='/cluster/user/import'), TableAction(name='Export', permission='read', icon=Icon.EXPORT.value, - click='authExport', disable=True), + click='authExport', disable=True) ], - column_key='entity', permissions=[Scope.CONFIG_OPT], - forms=[create_form, import_user_form], + forms=[create_form, edit_form, import_user_form], + column_key='entity', get_all=CRUDCollectionMethod( func=CephUserEndpoints.user_list, doc=EndpointDoc("Get Ceph Users") @@ -155,6 +199,10 @@ import_user_form = Form(path='/cluster/user/import', func=CephUserEndpoints.user_create, doc=EndpointDoc("Create Ceph User") ), + edit=CRUDCollectionMethod( + func=CephUserEndpoints.user_edit, + doc=EndpointDoc("Edit Ceph User") + ), delete=CRUDCollectionMethod( func=CephUserEndpoints.user_delete, doc=EndpointDoc("Delete Ceph User") 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 c053d51ef0bf9..fed34a3d2459a 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 @@ -141,6 +141,14 @@ const routes: Routes = [ resource: 'api.cluster.user@1.0' } }, + { + path: 'cluster/user/edit', + component: CrudFormComponent, + data: { + breadcrumbs: 'Cluster/Users', + resource: 'api.cluster.user@1.0' + } + }, { path: 'monitor', component: MonitorComponent, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/submit-button/submit-button.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/submit-button/submit-button.component.ts index 22e23d845eb95..595fb667b764b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/submit-button/submit-button.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/submit-button/submit-button.component.ts @@ -74,7 +74,7 @@ export class SubmitButtonComponent implements OnInit { (this.form).onSubmit($event); } - if (this.form.invalid) { + if (this.form?.invalid) { this.focusInvalid(); return; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.ts index bdb3b44bd73b1..750152161c246 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import _ from 'lodash'; @@ -52,7 +52,8 @@ export class CRUDTableComponent implements OnInit { private taskWrapper: TaskWrapperService, private cephUserService: CephUserService, private activatedRoute: ActivatedRoute, - private modalService: ModalService + private modalService: ModalService, + private router: Router ) { this.permissions = this.authStorageService.getPermissions(); } @@ -147,6 +148,14 @@ export class CRUDTableComponent implements OnInit { this.expandedRow = event; } + edit() { + let key = ''; + if (this.selection.hasSelection) { + key = this.selection.first()[this.meta.columnKey]; + } + this.router.navigate(['/cluster/user/edit'], { queryParams: { key: key } }); + } + authExport() { let entities: string[] = []; this.selection.selected.forEach((row) => entities.push(row.entity)); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.ts index 14ddccbc19579..9f03ec4a6de8d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { DataGatewayService } from '~/app/shared/services/data-gateway.service'; import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; import { FinishedTask } from '~/app/shared/models/finished-task'; @@ -9,6 +9,7 @@ import { mergeMap } from 'rxjs/operators'; import { CrudTaskInfo, JsonFormUISchema } from './crud-form.model'; import { Observable } from 'rxjs'; import _ from 'lodash'; +import { CdTableSelection } from '../../models/cd-table-selection'; @Component({ selector: 'cd-crud-form', @@ -21,22 +22,40 @@ export class CrudFormComponent implements OnInit { task: { message: string; id: string } = { message: '', id: '' }; form = new FormGroup({}); formUISchema$: Observable; + methodType: string; + urlFormName: string; + key: string = ''; + selected: CdTableSelection; constructor( private dataGatewayService: DataGatewayService, private activatedRoute: ActivatedRoute, private taskWrapper: TaskWrapperService, - private location: Location + private location: Location, + private router: Router ) {} ngOnInit(): void { - this.formUISchema$ = this.activatedRoute.data.pipe( - mergeMap((data: any) => { - this.resource = data.resource; - const url = '/' + this.activatedRoute.snapshot.url.join('/'); - return this.dataGatewayService.form(`ui-${this.resource}`, url); - }) - ); + this.activatedRoute.queryParamMap.subscribe((paramMap) => { + this.formUISchema$ = this.activatedRoute.data.pipe( + mergeMap((data: any) => { + this.resource = data.resource || this.resource; + const url = '/' + this.activatedRoute.snapshot.url.join('/'); + const key = paramMap.get('key') || ''; + return this.dataGatewayService.form(`ui-${this.resource}`, url, key); + }) + ); + this.formUISchema$.subscribe((data: any) => { + this.methodType = data.methodType; + this.model = data.model; + }); + this.urlFormName = this.router.url.split('/').pop(); + // remove optional arguments + const paramIndex = this.urlFormName.indexOf('?'); + if (paramIndex > 0) { + this.urlFormName = this.urlFormName.substring(0, paramIndex); + } + }); } async readFileAsText(file: File): Promise { @@ -72,8 +91,8 @@ export class CrudFormComponent implements OnInit { await this.preSubmit(data); this.taskWrapper .wrapTaskAroundCall({ - task: new FinishedTask('crud-component', taskMetadata), - call: this.dataGatewayService.create(this.resource, data) + task: new FinishedTask(`crud-component/${this.urlFormName}`, taskMetadata), + call: this.dataGatewayService.submit(this.resource, data, this.methodType) }) .subscribe({ complete: () => { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.model.ts index fbded8f24bc22..b0fcdfb60082f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.model.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.model.ts @@ -10,4 +10,6 @@ export interface JsonFormUISchema { controlSchema: FormlyFieldConfig[]; uiSchema: any; taskInfo: CrudTaskInfo; + methodType: string; + model: any; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/crud-form-adapter.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/crud-form-adapter.service.spec.ts index 7905a11e11aa2..bdefdd64180f0 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/crud-form-adapter.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/crud-form-adapter.service.spec.ts @@ -1,12 +1,15 @@ import { TestBed } from '@angular/core/testing'; import { CrudFormAdapterService } from './crud-form-adapter.service'; +import { RouterTestingModule } from '@angular/router/testing'; describe('CrudFormAdapterService', () => { let service: CrudFormAdapterService; beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ + imports: [RouterTestingModule] + }); service = TestBed.inject(CrudFormAdapterService); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/crud-form-adapter.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/crud-form-adapter.service.ts index ab0b7a6d6e64f..49c8fc9417b83 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/crud-form-adapter.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/crud-form-adapter.service.ts @@ -26,6 +26,7 @@ export class CrudFormAdapterService { for (let j = 0; j < uiSchema.length; j++) { if (controlSchema[i].key == uiSchema[j].key) { controlSchema[i].props.templateOptions = uiSchema[j].templateOptions; + controlSchema[i].props.readonly = uiSchema[j].readonly; setupValidators(controlSchema[i], uiSchema); } } @@ -34,6 +35,8 @@ export class CrudFormAdapterService { metadataFields: response.forms[form].task_info.metadataFields, message: response.forms[form].task_info.message }; - return { title, uiSchema, controlSchema, taskInfo }; + const methodType = response.forms[form].method_type; + const model = response.forms[form].model || {}; + return { title, uiSchema, controlSchema, taskInfo, methodType, model }; } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/data-gateway.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/data-gateway.service.spec.ts index 1eb7ccbc42492..96095dfe6fd4b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/data-gateway.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/data-gateway.service.spec.ts @@ -4,11 +4,12 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; import { DataGatewayService } from './data-gateway.service'; +import { RouterTestingModule } from '@angular/router/testing'; describe('Service: DataGateway', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule], + imports: [HttpClientTestingModule, RouterTestingModule], providers: [DataGatewayService] }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/data-gateway.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/data-gateway.service.ts index 15045d6d83198..c4a223e31b3e9 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/data-gateway.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/data-gateway.service.ts @@ -1,4 +1,4 @@ -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; @@ -11,6 +11,7 @@ import { CrudFormAdapterService } from './crud-form-adapter.service'; }) export class DataGatewayService { cache: { [keys: string]: Observable } = {}; + selected: any; constructor(private http: HttpClient, private crudFormAdapater: CrudFormAdapterService) {} @@ -27,10 +28,10 @@ export class DataGatewayService { return this.cache[cacheable]; } - create(dataPath: string, data: any): Observable { + submit(dataPath: string, data: any, methodType: string): Observable { const { url, version } = this.getUrlAndVersion(dataPath); - return this.http.post(url, data, { + return this.http[methodType](url, data, { headers: { Accept: `application/vnd.ceph.api.v${version}+json` } }); } @@ -44,13 +45,15 @@ export class DataGatewayService { }); } - form(dataPath: string, formPath: string): Observable { - const cacheable = this.getCacheable(dataPath, 'get'); + form(dataPath: string, formPath: string, modelKey: string = ''): Observable { + const cacheable = this.getCacheable(dataPath, 'get', modelKey); + const params = { model_key: modelKey }; if (this.cache[cacheable] === undefined) { const { url, version } = this.getUrlAndVersion(dataPath); this.cache[cacheable] = this.http.get(url, { - headers: { Accept: `application/vnd.ceph.api.v${version}+json` } + headers: { Accept: `application/vnd.ceph.api.v${version}+json` }, + params: params }); } return this.cache[cacheable].pipe( @@ -60,8 +63,21 @@ export class DataGatewayService { ); } - getCacheable(dataPath: string, method: string) { - return dataPath + method; + model(dataPath: string, params: HttpParams): Observable { + const cacheable = this.getCacheable(dataPath, 'get'); + if (this.cache[cacheable] === undefined) { + const { url, version } = this.getUrlAndVersion(dataPath); + + this.cache[cacheable] = this.http.get(`${url}/model`, { + headers: { Accept: `application/vnd.ceph.api.v${version}+json` }, + params: params + }); + } + return this.cache[cacheable]; + } + + getCacheable(dataPath: string, method: string, key: string = '') { + return dataPath + method + key; } getUrlAndVersion(dataPath: string) { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts index 195f3b1374ab3..bc11a0be39cac 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts @@ -341,7 +341,13 @@ export class TaskMessageService { 'service/delete': this.newTaskMessage(this.commonOperations.delete, (metadata) => this.service(metadata) ), - 'crud-component': this.newTaskMessage(this.commonOperations.create, (metadata) => + 'crud-component/create': this.newTaskMessage(this.commonOperations.create, (metadata) => + this.crudMessage(metadata) + ), + 'crud-component/edit': this.newTaskMessage(this.commonOperations.update, (metadata) => + this.crudMessage(metadata) + ), + 'crud-component/import': this.newTaskMessage(this.commonOperations.import, (metadata) => this.crudMessage(metadata) ), 'crud-component/id': this.newTaskMessage(this.commonOperations.delete, (id) => diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index 21be14568fd4e..9dcbdeb35aadc 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -2143,6 +2143,28 @@ paths: summary: Update the cluster status tags: - Cluster + /api/cluster/capacity: + get: + parameters: [] + responses: + '200': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: OK + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + tags: + - Cluster /api/cluster/user: get: description: "\n Get list of ceph users and its respective data\n \ @@ -2212,6 +2234,48 @@ paths: summary: Create Ceph User tags: - Cluster + put: + description: "\n Change the ceph user capabilities.\n Setting\ + \ new capabilities will overwrite current ones.\n :param user_entity:\ + \ Entity to change\n :param capabilities: List of updated capabilities\ + \ to user_entity\n " + parameters: [] + requestBody: + content: + application/json: + schema: + properties: + capabilities: + type: string + user_entity: + default: '' + type: string + type: object + responses: + '200': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: Resource updated. + '202': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: Operation is still executing. Please check the task queue. + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + summary: Edit Ceph User + tags: + - Cluster /api/cluster/user/export: post: parameters: [] @@ -2253,7 +2317,7 @@ paths: /api/cluster/user/{user_entity}: delete: description: "\n Delete a ceph user and it's defined capabilities.\n\ - \ :param user_entity: Entity to dlelete\n " + \ :param user_entity: Entity to delete\n " parameters: - in: path name: user_entity -- 2.39.5