DESTROY = 'fa fa-times'
IMPORT = 'fa fa-upload'
EXPORT = 'fa fa-download'
+ EDIT = 'fa fa-pencil'
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.
"""
field_type: Any = str
default_value: Optional[Any] = None
optional: bool = False
+ readonly: bool = False
help: str = ''
validators: List[Validator] = []
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):
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)
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
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
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 []
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)
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()
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)
import logging
+from enum import Enum
from errno import EINVAL
from typing import List, NamedTuple, Optional
cap: str
+class MethodType(Enum):
+ POST = 'post'
+ PUT = 'put'
+
+
class CephUserEndpoints:
@staticmethod
def _run_auth_command(command: str, *args, **kwargs):
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
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',
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
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(
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")
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")
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,
(<FormGroupDirective>this.form).onSubmit($event);
}
- if (this.form.invalid) {
+ if (this.form?.invalid) {
this.focusInvalid();
return;
}
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';
private taskWrapper: TaskWrapperService,
private cephUserService: CephUserService,
private activatedRoute: ActivatedRoute,
- private modalService: ModalService
+ private modalService: ModalService,
+ private router: Router
) {
this.permissions = this.authStorageService.getPermissions();
}
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));
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';
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',
task: { message: string; id: string } = { message: '', id: '' };
form = new FormGroup({});
formUISchema$: Observable<JsonFormUISchema>;
+ 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<string> {
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: () => {
controlSchema: FormlyFieldConfig[];
uiSchema: any;
taskInfo: CrudTaskInfo;
+ methodType: string;
+ model: any;
}
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);
});
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);
}
}
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 };
}
}
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]
});
});
-import { HttpClient } from '@angular/common/http';
+import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
})
export class DataGatewayService {
cache: { [keys: string]: Observable<any> } = {};
+ selected: any;
constructor(private http: HttpClient, private crudFormAdapater: CrudFormAdapterService) {}
return this.cache[cacheable];
}
- create(dataPath: string, data: any): Observable<any> {
+ submit(dataPath: string, data: any, methodType: string): Observable<any> {
const { url, version } = this.getUrlAndVersion(dataPath);
- return this.http.post<any>(url, data, {
+ return this.http[methodType]<any>(url, data, {
headers: { Accept: `application/vnd.ceph.api.v${version}+json` }
});
}
});
}
- form(dataPath: string, formPath: string): Observable<JsonFormUISchema> {
- const cacheable = this.getCacheable(dataPath, 'get');
+ form(dataPath: string, formPath: string, modelKey: string = ''): Observable<JsonFormUISchema> {
+ 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<any>(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(
);
}
- getCacheable(dataPath: string, method: string) {
- return dataPath + method;
+ model(dataPath: string, params: HttpParams): Observable<any> {
+ const cacheable = this.getCacheable(dataPath, 'get');
+ if (this.cache[cacheable] === undefined) {
+ const { url, version } = this.getUrlAndVersion(dataPath);
+
+ this.cache[cacheable] = this.http.get<any>(`${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) {
'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) =>
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 \
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: []
/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