From 67edf3a0fd6d1bff6b11eb662b95d865b25bb3d6 Mon Sep 17 00:00:00 2001 From: Pere Diaz Bou Date: Thu, 2 Mar 2023 13:17:25 +0100 Subject: [PATCH] mgr/dashboard: rgw role creation form Fixes: https://tracker.ceph.com/issues/59187 Signed-off-by: Pere Diaz Bou Signed-off-by: Nizamudeen A (cherry picked from commit bd0eb20c673d54b9be3440decc0f3a1449153385) --- src/pybind/mgr/dashboard/controllers/_crud.py | 60 +++++++++++++------ .../mgr/dashboard/controllers/ceph_users.py | 8 ++- src/pybind/mgr/dashboard/controllers/rgw.py | 45 +++++++++++++- .../frontend/src/app/ceph/rgw/rgw.module.ts | 17 +++++- .../crud-table/crud-table.component.html | 6 +- .../crud-table/crud-table.component.ts | 8 +++ .../app/shared/datatable/datatable.module.ts | 26 +++++++- .../forms/crud-form/crud-form.component.html | 6 +- .../forms/crud-form/crud-form.component.ts | 17 +++--- .../shared/forms/crud-form/crud-form.model.ts | 6 ++ .../formly-input-type.component.html | 4 +- .../formly-input-wrapper.component.html | 37 ++++++++++++ .../formly-input-wrapper.component.scss | 0 .../formly-input-wrapper.component.spec.ts | 47 +++++++++++++++ .../formly-input-wrapper.component.ts | 15 +++++ .../formly-textarea-type.component.html | 9 +++ .../formly-textarea-type.component.scss | 0 .../formly-textarea-type.component.spec.ts | 47 +++++++++++++++ .../formly-textarea-type.component.ts | 25 ++++++++ .../src/app/shared/forms/crud-form/helpers.ts | 35 +++++++++++ .../crud-form/validators/json-validator.ts | 12 ++++ .../validators/rgw-role-validator.ts | 19 ++++++ .../services/crud-form-adapter.service.ts | 11 +++- .../shared/services/task-message.service.ts | 16 +++-- .../frontend/src/app/shared/shared.module.ts | 3 +- .../mgr/dashboard/frontend/src/styles.scss | 3 +- src/pybind/mgr/dashboard/openapi.yaml | 42 +++++++++++++ .../mgr/dashboard/services/rgw_client.py | 45 ++++++++++++++ 28 files changed, 519 insertions(+), 50 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-wrapper/formly-input-wrapper.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-wrapper/formly-input-wrapper.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-wrapper/formly-input-wrapper.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-wrapper/formly-input-wrapper.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-textarea-type/formly-textarea-type.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-textarea-type/formly-textarea-type.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-textarea-type/formly-textarea-type.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-textarea-type/formly-textarea-type.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/helpers.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/validators/json-validator.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/validators/rgw-role-validator.ts diff --git a/src/pybind/mgr/dashboard/controllers/_crud.py b/src/pybind/mgr/dashboard/controllers/_crud.py index cbf87e54be375..63224928b1bbd 100644 --- a/src/pybind/mgr/dashboard/controllers/_crud.py +++ b/src/pybind/mgr/dashboard/controllers/_crud.py @@ -78,6 +78,12 @@ class Icon(Enum): add = 'fa fa-plus' +class Validator(Enum): + JSON = 'json' + RGW_ROLE_NAME = 'rgwRoleName' + RGW_ROLE_PATH = 'rgwRolePath' + + class FormField(NamedTuple): """ The key of a FromField is then used to send the data related to that key into the @@ -89,6 +95,8 @@ class FormField(NamedTuple): field_type: Any = str default_value: Optional[Any] = None optional: bool = False + help: str = '' + validators: List[Validator] = [] def get_type(self): _type = '' @@ -98,6 +106,8 @@ class FormField(NamedTuple): _type = 'int' elif self.field_type == bool: _type = 'boolean' + elif self.field_type == 'textarea': + _type = 'textarea' else: raise NotImplementedError(f'Unimplemented type {self.field_type}') return _type @@ -173,7 +183,7 @@ class Container: # include fields in this container's schema for field in self.fields: - field_ui_schema = {} + field_ui_schema: Dict[str, Any] = {} properties[field.key] = {} field_key = field.key if key: @@ -187,6 +197,8 @@ class Container: properties[field.key]['type'] = _type properties[field.key]['title'] = field.name field_ui_schema['key'] = field_key + field_ui_schema['help'] = f'{field.help}' + field_ui_schema['validators'] = [i.value for i in field.validators] items.append(field_ui_schema) elif isinstance(field, Container): container_schema = field.to_dict(key+'.'+field.key if key else field.key) @@ -232,13 +244,26 @@ class ArrayHorizontalContainer(Container): return 'array' +class FormTaskInfo: + def __init__(self, message: str, metadata_fields: List[str]) -> None: + self.message = message + self.metadata_fields = metadata_fields + + def to_dict(self): + return {'message': self.message, 'metadataFields': self.metadata_fields} + + class Form: - def __init__(self, path, root_container): + def __init__(self, path, root_container, + task_info: FormTaskInfo = FormTaskInfo("Unknown task", [])): self.path = path - self.root_container = root_container + self.root_container: Container = root_container + self.task_info = task_info def to_dict(self): - return self.root_container.to_dict() + res = self.root_container.to_dict() + res['task_info'] = self.task_info.to_dict() + return res class CRUDMeta(SerializableClass): @@ -247,6 +272,7 @@ class CRUDMeta(SerializableClass): self.permissions = [] self.actions = [] self.forms = [] + self.detail_columns = [] class CRUDCollectionMethod(NamedTuple): @@ -270,26 +296,18 @@ class CRUDEndpoint: actions: Optional[List[TableAction]] = None, permissions: Optional[List[str]] = None, forms: Optional[List[Form]] = None, meta: CRUDMeta = CRUDMeta(), get_all: Optional[CRUDCollectionMethod] = None, - create: Optional[CRUDCollectionMethod] = None): + create: Optional[CRUDCollectionMethod] = None, + detail_columns: Optional[List[str]] = None): self.router = router self.doc = doc self.set_column = set_column - if actions: - self.actions = actions - else: - self.actions = [] - - if forms: - self.forms = forms - else: - self.forms = [] + self.actions = actions if actions is not None else [] + self.forms = forms if forms is not None else [] self.meta = meta self.get_all = get_all self.create = create - if permissions: - self.permissions = permissions - else: - self.permissions = [] + self.permissions = permissions if permissions is not None else [] + self.detail_columns = detail_columns if detail_columns is not None else [] def __call__(self, cls: Any): self.create_crud_class(cls) @@ -328,8 +346,13 @@ class CRUDEndpoint: self.generate_actions() self.generate_forms() self.set_permissions() + self.get_detail_columns() return serialize(self.__class__.outer_self.meta) + def get_detail_columns(self): + columns = self.__class__.outer_self.detail_columns + self.__class__.outer_self.meta.detail_columns = columns + def update_columns(self): if self.__class__.outer_self.set_column: for i, column in enumerate(self.__class__.outer_self.meta.table.columns): @@ -371,6 +394,7 @@ class CRUDEndpoint: 'generate_actions': generate_actions, 'generate_forms': generate_forms, 'set_permissions': set_permissions, + 'get_detail_columns': get_detail_columns, 'outer_self': self, }) UIRouter(self.router.path, self.router.security_scope)(meta_class) diff --git a/src/pybind/mgr/dashboard/controllers/ceph_users.py b/src/pybind/mgr/dashboard/controllers/ceph_users.py index 344ad4c04c1f5..2de1d36c78b5c 100644 --- a/src/pybind/mgr/dashboard/controllers/ceph_users.py +++ b/src/pybind/mgr/dashboard/controllers/ceph_users.py @@ -6,8 +6,8 @@ from ..exceptions import DashboardException from ..security import Scope from ..services.ceph_service import CephService, SendCommandError from . import APIDoc, APIRouter, CRUDCollectionMethod, CRUDEndpoint, EndpointDoc, SecretStr -from ._crud import ArrayHorizontalContainer, CRUDMeta, Form, FormField, Icon, \ - TableAction, VerticalContainer +from ._crud import ArrayHorizontalContainer, CRUDMeta, Form, FormField, \ + FormTaskInfo, Icon, TableAction, VerticalContainer logger = logging.getLogger("controllers.ceph_users") @@ -71,7 +71,9 @@ create_container = VerticalContainer('Create User', 'create_user', fields=[ ]) create_form = Form(path='/cluster/user/create', - root_container=create_container) + root_container=create_container, + task_info=FormTaskInfo("Ceph user '{user_entity}' created successfully", + ['user_entity'])) @CRUDEndpoint( diff --git a/src/pybind/mgr/dashboard/controllers/rgw.py b/src/pybind/mgr/dashboard/controllers/rgw.py index 710a4980f7cd3..85af1ae7fbaee 100644 --- a/src/pybind/mgr/dashboard/controllers/rgw.py +++ b/src/pybind/mgr/dashboard/controllers/rgw.py @@ -16,7 +16,8 @@ from ..tools import json_str_to_object, str_to_bool from . import APIDoc, APIRouter, BaseController, CRUDCollectionMethod, \ CRUDEndpoint, Endpoint, EndpointDoc, ReadPermission, RESTController, \ UIRouter, allow_empty_body -from ._crud import CRUDMeta +from ._crud import CRUDMeta, Form, FormField, FormTaskInfo, Icon, TableAction, \ + Validator, VerticalContainer from ._version import APIVersion logger = logging.getLogger("controllers.rgw") @@ -614,21 +615,61 @@ class RGWRoleEndpoints: roles = rgw_client.list_roles() return roles + @staticmethod + def role_create(_, role_name: str = '', role_path: str = '', role_assume_policy_doc: str = ''): + assert role_name + assert role_path + rgw_client = RgwClient.admin_instance() + rgw_client.create_role(role_name, role_path, role_assume_policy_doc) + return f'Role {role_name} created successfully' + + +# pylint: disable=C0301 +assume_role_policy_help = ( + 'Paste a json assume role policy document, to find more information on how to get this document, click here.' +) + +create_container = VerticalContainer('Create Role', 'create_role', fields=[ + FormField('Role name', 'role_name', validators=[Validator.RGW_ROLE_NAME]), + FormField('Path', 'role_path', validators=[Validator.RGW_ROLE_PATH]), + FormField('Assume Role Policy Document', + 'role_assume_policy_doc', + help=assume_role_policy_help, + field_type='textarea', + validators=[Validator.JSON]), +]) +create_role_form = Form(path='/rgw/user/roles/create', + root_container=create_container, + task_info=FormTaskInfo("IAM RGW Role '{role_name}' created successfully", + ['role_name'])) + @CRUDEndpoint( router=APIRouter('/rgw/user/roles', Scope.RGW), doc=APIDoc("List of RGW roles", "RGW"), - actions=[], + actions=[ + TableAction(name='Create', permission='create', icon=Icon.add.value, + routerLink='/rgw/user/roles/create') + ], + forms=[create_role_form], permissions=[Scope.CONFIG_OPT], get_all=CRUDCollectionMethod( func=RGWRoleEndpoints.role_list, doc=EndpointDoc("List RGW roles") ), + create=CRUDCollectionMethod( + func=RGWRoleEndpoints.role_create, + doc=EndpointDoc("Create Ceph User") + ), set_column={ "CreateDate": {'cellTemplate': 'date'}, "MaxSessionDuration": {'cellTemplate': 'duration'}, + "RoleId": {'isHidden': True}, "AssumeRolePolicyDocument": {'isHidden': True} }, + detail_columns=['RoleId', 'AssumeRolePolicyDocument'], meta=CRUDMeta() ) class RgwUserRole(NamedTuple): diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts index edffa8e856b75..e667b6d39888a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts @@ -24,6 +24,7 @@ import { RgwUserS3KeyModalComponent } from './rgw-user-s3-key-modal/rgw-user-s3- import { RgwUserSubuserModalComponent } from './rgw-user-subuser-modal/rgw-user-subuser-modal.component'; import { RgwUserSwiftKeyModalComponent } from './rgw-user-swift-key-modal/rgw-user-swift-key-modal.component'; import { RgwUserTabsComponent } from './rgw-user-tabs/rgw-user-tabs.component'; +import { CrudFormComponent } from '~/app/shared/forms/crud-form/crud-form.component'; @NgModule({ imports: [ @@ -88,7 +89,6 @@ const routes: Routes = [ }, { path: 'roles', - component: CRUDTableComponent, data: { breadcrumbs: 'Roles', resource: 'api.rgw.user.roles@1.0', @@ -102,7 +102,20 @@ const routes: Routes = [ url: '/rgw/user/roles' } ] - } + }, + children: [ + { + path: '', + component: CRUDTableComponent + }, + { + path: URLVerbs.CREATE, + component: CrudFormComponent, + data: { + breadcrumbs: ActionLabels.CREATE + } + } + ] } ] }, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.html index d1aed4462d7f7..5b7c58bc1c7c0 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.html @@ -16,7 +16,11 @@ [data]="data$ | async" [columns]="meta.table.columns" [columnMode]="meta.table.columnMode" - [toolHeader]="meta.table.toolHeader"> + [toolHeader]="meta.table.toolHeader" + selectionType="single" + (setExpandedRow)="setExpandedRow($event)" + (updateSelection)="updateSelection($event)" + [hasDetails]="meta.detail_columns.length > 0">
{{ formUISchema.title }}
+ (ngSubmit)="submit(model, formUISchema.taskInfo)">
+ [options]="{formState: formUISchema.uiSchema}">