]>
git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/controllers/_crud.py
2 from functools
import wraps
3 from inspect
import isclass
4 from typing
import Any
, Callable
, Dict
, Generator
, Iterable
, Iterator
, List
, \
5 NamedTuple
, Optional
, Tuple
, Union
, get_type_hints
7 from ._api
_router
import APIRouter
8 from ._docs
import APIDoc
, EndpointDoc
9 from ._rest
_controller
import RESTController
10 from ._ui
_router
import UIRouter
17 class MethodType(Enum
):
23 return isinstance(o
, tuple) and hasattr(o
, '_asdict') and hasattr(o
, '_fields')
26 class SerializableClass
:
28 for attr
in self
.__dict
__:
29 if not attr
.startswith("__"):
30 yield attr
, getattr(self
, attr
)
32 def __contains__(self
, value
):
33 return value
in self
.__dict
__
36 return len(self
.__dict
__)
39 def serialize(o
, expected_type
=None):
40 # pylint: disable=R1705,W1116
42 hints
= get_type_hints(o
)
43 return {k
: serialize(v
, hints
[k
]) for k
, v
in zip(o
._fields
, o
)}
44 elif isinstance(o
, (list, tuple, set)):
45 # json serializes list and tuples to arrays, hence we also serialize
47 # NOTE: we could add a metadata value in a list to indentify tuples and,
48 # sets if we wanted but for now let's go for lists.
49 return [serialize(i
) for i
in o
]
50 elif isinstance(o
, SerializableClass
):
51 return {serialize(k
): serialize(v
) for k
, v
in o
}
52 elif isinstance(o
, (Iterator
, Generator
)):
53 return [serialize(i
) for i
in o
]
54 elif expected_type
and isclass(expected_type
) and issubclass(expected_type
, SecretStr
):
60 class TableColumn(NamedTuple
):
62 cellTemplate
: str = ''
63 isHidden
: bool = False
64 filterable
: bool = True
68 class TableAction(NamedTuple
):
72 routerLink
: str = '' # redirect to...
74 disable
: bool = False # disable without selection
77 class SelectionType(Enum
):
83 class TableComponent(SerializableClass
):
84 def __init__(self
) -> None:
85 self
.columns
: List
[TableColumn
] = []
86 self
.columnMode
: str = 'flex'
87 self
.toolHeader
: bool = True
88 self
.selectionType
: str = SelectionType
.SINGLE
.value
90 def set_selection_type(self
, type_
: SelectionType
):
91 self
.selectionType
= type_
.value
96 DESTROY
= 'fa fa-times'
97 IMPORT
= 'fa fa-upload'
98 EXPORT
= 'fa fa-download'
102 class Validator(Enum
):
104 RGW_ROLE_NAME
= 'rgwRoleName'
105 RGW_ROLE_PATH
= 'rgwRolePath'
109 class FormField(NamedTuple
):
111 The key of a FormField is then used to send the data related to that key into the
112 POST and PUT endpoints. It is imperative for the developer to map keys of fields and containers
113 to the input of the POST and PUT endpoints.
117 field_type
: Any
= str
118 default_value
: Optional
[Any
] = None
119 optional
: bool = False
120 readonly
: bool = False
122 validators
: List
[Validator
] = []
126 if self
.field_type
== str:
128 elif self
.field_type
== int:
130 elif self
.field_type
== bool:
132 elif self
.field_type
== 'textarea':
134 elif self
.field_type
== "file":
137 raise NotImplementedError(f
'Unimplemented type {self.field_type}')
142 def __init__(self
, name
: str, key
: str, fields
: List
[Union
[FormField
, "Container"]],
143 optional
: bool = False, readonly
: bool = False, min_items
=1):
147 self
.optional
= optional
148 self
.readonly
= readonly
149 self
.min_items
= min_items
151 def layout_type(self
):
152 raise NotImplementedError
154 def _property_type(self
):
155 raise NotImplementedError
157 def to_dict(self
, key
=''):
158 # intialize the schema of this container
161 'type': self
._property
_type
(),
164 items
= None # layout items alias as it depends on the type of container
165 properties
= None # control schema properties alias
167 if self
._property
_type
() == 'array':
168 control_schema
['required'] = []
169 control_schema
['minItems'] = self
.min_items
170 control_schema
['items'] = {
175 properties
= control_schema
['items']['properties']
176 required
= control_schema
['required']
177 control_schema
['items']['required'] = required
182 'objectTemplateOptions': {
183 'layoutType': self
.layout_type()
188 items
= ui_schemas
[-1]['items']
190 control_schema
['properties'] = {}
191 control_schema
['required'] = []
192 required
= control_schema
['required']
193 properties
= control_schema
['properties']
196 'layoutType': self
.layout_type()
202 items
= ui_schemas
[-1]['items']
206 assert items
is not None
207 assert properties
is not None
208 assert required
is not None
210 # include fields in this container's schema
211 for field
in self
.fields
:
212 field_ui_schema
: Dict
[str, Any
] = {}
213 properties
[field
.key
] = {}
214 field_key
= field
.key
216 if self
._property
_type
() == 'array':
217 field_key
= key
+ '[].' + field
.key
219 field_key
= key
+ '.' + field
.key
221 if isinstance(field
, FormField
):
222 _type
= field
.get_type()
223 properties
[field
.key
]['type'] = _type
224 properties
[field
.key
]['title'] = field
.name
225 field_ui_schema
['key'] = field_key
226 field_ui_schema
['readonly'] = field
.readonly
227 field_ui_schema
['help'] = f
'{field.help}'
228 field_ui_schema
['validators'] = [i
.value
for i
in field
.validators
]
229 items
.append(field_ui_schema
)
230 elif isinstance(field
, Container
):
231 container_schema
= field
.to_dict(key
+'.'+field
.key
if key
else field
.key
)
232 properties
[field
.key
] = container_schema
['control_schema']
233 ui_schemas
.extend(container_schema
['ui_schema'])
234 if not field
.optional
:
235 required
.append(field
.key
)
237 'ui_schema': ui_schemas
,
238 'control_schema': control_schema
,
242 class VerticalContainer(Container
):
243 def layout_type(self
):
246 def _property_type(self
):
250 class HorizontalContainer(Container
):
251 def layout_type(self
):
254 def _property_type(self
):
258 class ArrayVerticalContainer(Container
):
259 def layout_type(self
):
262 def _property_type(self
):
266 class ArrayHorizontalContainer(Container
):
267 def layout_type(self
):
270 def _property_type(self
):
275 def __init__(self
, message
: str, metadata_fields
: List
[str]) -> None:
276 self
.message
= message
277 self
.metadata_fields
= metadata_fields
280 return {'message': self
.message
, 'metadataFields': self
.metadata_fields
}
284 def __init__(self
, path
, root_container
, method_type
='',
285 task_info
: FormTaskInfo
= FormTaskInfo("Unknown task", []),
286 model_callback
=None):
288 self
.root_container
: Container
= root_container
289 self
.method_type
= method_type
290 self
.task_info
= task_info
291 self
.model_callback
= model_callback
294 res
= self
.root_container
.to_dict()
295 res
['method_type'] = self
.method_type
296 res
['task_info'] = self
.task_info
.to_dict()
297 res
['path'] = self
.path
298 res
['ask'] = self
.path
302 class CRUDMeta(SerializableClass
):
304 self
.table
= TableComponent()
305 self
.permissions
= []
309 self
.detail_columns
= []
312 class CRUDCollectionMethod(NamedTuple
):
313 func
: Callable
[..., Iterable
[Any
]]
317 class CRUDResourceMethod(NamedTuple
):
318 func
: Callable
[..., Any
]
322 # pylint: disable=R0902
324 # for testing purposes
325 CRUDClass
: Optional
[RESTController
] = None
326 CRUDClassMetadata
: Optional
[RESTController
] = None
328 def __init__(self
, router
: APIRouter
, doc
: APIDoc
,
329 set_column
: Optional
[Dict
[str, Dict
[str, str]]] = None,
330 actions
: Optional
[List
[TableAction
]] = None,
331 permissions
: Optional
[List
[str]] = None, forms
: Optional
[List
[Form
]] = None,
332 column_key
: Optional
[str] = None,
333 meta
: CRUDMeta
= CRUDMeta(), get_all
: Optional
[CRUDCollectionMethod
] = None,
334 create
: Optional
[CRUDCollectionMethod
] = None,
335 delete
: Optional
[CRUDCollectionMethod
] = None,
336 selection_type
: SelectionType
= SelectionType
.SINGLE
,
337 extra_endpoints
: Optional
[List
[Tuple
[str, CRUDCollectionMethod
]]] = None,
338 edit
: Optional
[CRUDCollectionMethod
] = None,
339 detail_columns
: Optional
[List
[str]] = None):
342 self
.set_column
= set_column
343 self
.actions
= actions
if actions
is not None else []
344 self
.forms
= forms
if forms
is not None else []
346 self
.get_all
= get_all
350 self
.permissions
= permissions
if permissions
is not None else []
351 self
.column_key
= column_key
if column_key
is not None else ''
352 self
.detail_columns
= detail_columns
if detail_columns
is not None else []
353 self
.extra_endpoints
= extra_endpoints
if extra_endpoints
is not None else []
354 self
.selection_type
= selection_type
356 def __call__(self
, cls
: Any
):
357 self
.create_crud_class(cls
)
359 self
.meta
.table
.columns
.extend(TableColumn(prop
=field
) for field
in cls
._fields
)
360 self
.create_meta_class(cls
)
363 def create_crud_class(self
, cls
):
364 outer_self
: CRUDEndpoint
= self
369 @wraps(self
.get_all
.func
)
370 def _list(self
, *args
, **kwargs
):
372 for item
in outer_self
.get_all
.func(self
, *args
, **kwargs
): # type: ignore
373 items
.append(serialize(cls(**item
)))
375 funcs
['list'] = _list
379 @wraps(self
.create
.func
)
380 def _create(self
, *args
, **kwargs
):
381 return outer_self
.create
.func(self
, *args
, **kwargs
) # type: ignore
382 funcs
['create'] = _create
386 @wraps(self
.delete
.func
)
387 def delete(self
, *args
, **kwargs
):
388 return outer_self
.delete
.func(self
, *args
, **kwargs
) # type: ignore
389 funcs
['delete'] = delete
393 @wraps(self
.edit
.func
)
394 def singleton_set(self
, *args
, **kwargs
):
395 return outer_self
.edit
.func(self
, *args
, **kwargs
) # type: ignore
396 funcs
['singleton_set'] = singleton_set
398 for extra_endpoint
in self
.extra_endpoints
:
399 funcs
[extra_endpoint
[0]] = extra_endpoint
[1].doc(extra_endpoint
[1].func
)
401 class_name
= self
.router
.path
.replace('/', '')
402 crud_class
= type(f
'{class_name}_CRUDClass',
408 self
.router(self
.doc(crud_class
))
409 cls
.CRUDClass
= crud_class
411 def create_meta_class(self
, cls
):
412 def _list(self
, model_key
: str = ''):
413 self
.update_columns()
414 self
.generate_actions()
415 self
.generate_forms(model_key
)
416 self
.set_permissions()
417 self
.set_column_key()
418 self
.get_detail_columns()
419 selection_type
= self
.__class
__.outer_self
.selection_type
420 self
.__class
__.outer_self
.meta
.table
.set_selection_type(selection_type
)
421 return serialize(self
.__class
__.outer_self
.meta
)
423 def get_detail_columns(self
):
424 columns
= self
.__class
__.outer_self
.detail_columns
425 self
.__class
__.outer_self
.meta
.detail_columns
= columns
427 def update_columns(self
):
428 if self
.__class
__.outer_self
.set_column
:
429 for i
, column
in enumerate(self
.__class
__.outer_self
.meta
.table
.columns
):
430 if column
.prop
in dict(self
.__class
__.outer_self
.set_column
):
431 prop
= self
.__class
__.outer_self
.set_column
[column
.prop
]
433 if "cellTemplate" in prop
:
434 new_template
= prop
["cellTemplate"]
435 hidden
= prop
['isHidden'] if 'isHidden' in prop
else False
436 flex_grow
= prop
['flexGrow'] if 'flexGrow' in prop
else column
.flexGrow
437 new_column
= TableColumn(column
.prop
,
442 self
.__class
__.outer_self
.meta
.table
.columns
[i
] = new_column
444 def generate_actions(self
):
445 self
.__class
__.outer_self
.meta
.actions
.clear()
447 for action
in self
.__class
__.outer_self
.actions
:
448 self
.__class
__.outer_self
.meta
.actions
.append(action
._asdict
())
450 def generate_forms(self
, model_key
):
451 self
.__class
__.outer_self
.meta
.forms
.clear()
453 for form
in self
.__class
__.outer_self
.forms
:
454 form_as_dict
= form
.to_dict()
456 if form
.model_callback
and model_key
:
457 model
= form
.model_callback(model_key
)
458 form_as_dict
['model'] = model
459 self
.__class
__.outer_self
.meta
.forms
.append(form_as_dict
)
461 def set_permissions(self
):
462 self
.__class
__.outer_self
.meta
.permissions
.clear()
464 if self
.__class
__.outer_self
.permissions
:
465 self
.outer_self
.meta
.permissions
.extend(self
.__class
__.outer_self
.permissions
)
467 def set_column_key(self
):
468 if self
.__class
__.outer_self
.column_key
:
469 self
.outer_self
.meta
.columnKey
= self
.__class
__.outer_self
.column_key
471 class_name
= self
.router
.path
.replace('/', '')
472 meta_class
= type(f
'{class_name}_CRUDClassMetadata',
476 'update_columns': update_columns
,
477 'generate_actions': generate_actions
,
478 'generate_forms': generate_forms
,
479 'set_permissions': set_permissions
,
480 'set_column_key': set_column_key
,
481 'get_detail_columns': get_detail_columns
,
484 UIRouter(self
.router
.path
, self
.router
.security_scope
)(meta_class
)
485 cls
.CRUDClassMetadata
= meta_class