]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/controllers/_crud.py
bump version to 18.2.4-pve3
[ceph.git] / ceph / src / pybind / mgr / dashboard / controllers / _crud.py
1 from enum import Enum
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
6
7 from ._api_router import APIRouter
8 from ._docs import APIDoc, EndpointDoc
9 from ._rest_controller import RESTController
10 from ._ui_router import UIRouter
11
12
13 class SecretStr(str):
14 pass
15
16
17 class MethodType(Enum):
18 POST = 'post'
19 PUT = 'put'
20
21
22 def isnamedtuple(o):
23 return isinstance(o, tuple) and hasattr(o, '_asdict') and hasattr(o, '_fields')
24
25
26 class SerializableClass:
27 def __iter__(self):
28 for attr in self.__dict__:
29 if not attr.startswith("__"):
30 yield attr, getattr(self, attr)
31
32 def __contains__(self, value):
33 return value in self.__dict__
34
35 def __len__(self):
36 return len(self.__dict__)
37
38
39 def serialize(o, expected_type=None):
40 # pylint: disable=R1705,W1116
41 if isnamedtuple(o):
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
46 # sets to lists.
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):
55 return "***********"
56 else:
57 return o
58
59
60 class TableColumn(NamedTuple):
61 prop: str
62 cellTemplate: str = ''
63 isHidden: bool = False
64 filterable: bool = True
65 flexGrow: int = 1
66
67
68 class TableAction(NamedTuple):
69 name: str
70 permission: str
71 icon: str
72 routerLink: str = '' # redirect to...
73 click: str = ''
74 disable: bool = False # disable without selection
75
76
77 class SelectionType(Enum):
78 NONE = ''
79 SINGLE = 'single'
80 MULTI = 'multiClick'
81
82
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
89
90 def set_selection_type(self, type_: SelectionType):
91 self.selectionType = type_.value
92
93
94 class Icon(Enum):
95 ADD = 'fa fa-plus'
96 DESTROY = 'fa fa-times'
97 IMPORT = 'fa fa-upload'
98 EXPORT = 'fa fa-download'
99 EDIT = 'fa fa-pencil'
100
101
102 class Validator(Enum):
103 JSON = 'json'
104 RGW_ROLE_NAME = 'rgwRoleName'
105 RGW_ROLE_PATH = 'rgwRolePath'
106 FILE = 'file'
107
108
109 class FormField(NamedTuple):
110 """
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.
114 """
115 name: str
116 key: str
117 field_type: Any = str
118 default_value: Optional[Any] = None
119 optional: bool = False
120 readonly: bool = False
121 help: str = ''
122 validators: List[Validator] = []
123
124 def get_type(self):
125 _type = ''
126 if self.field_type == str:
127 _type = 'string'
128 elif self.field_type == int:
129 _type = 'int'
130 elif self.field_type == bool:
131 _type = 'boolean'
132 elif self.field_type == 'textarea':
133 _type = 'textarea'
134 elif self.field_type == "file":
135 _type = 'file'
136 else:
137 raise NotImplementedError(f'Unimplemented type {self.field_type}')
138 return _type
139
140
141 class Container:
142 def __init__(self, name: str, key: str, fields: List[Union[FormField, "Container"]],
143 optional: bool = False, readonly: bool = False, min_items=1):
144 self.name = name
145 self.key = key
146 self.fields = fields
147 self.optional = optional
148 self.readonly = readonly
149 self.min_items = min_items
150
151 def layout_type(self):
152 raise NotImplementedError
153
154 def _property_type(self):
155 raise NotImplementedError
156
157 def to_dict(self, key=''):
158 # intialize the schema of this container
159 ui_schemas = []
160 control_schema = {
161 'type': self._property_type(),
162 'title': self.name
163 }
164 items = None # layout items alias as it depends on the type of container
165 properties = None # control schema properties alias
166 required = None
167 if self._property_type() == 'array':
168 control_schema['required'] = []
169 control_schema['minItems'] = self.min_items
170 control_schema['items'] = {
171 'type': 'object',
172 'properties': {},
173 'required': []
174 }
175 properties = control_schema['items']['properties']
176 required = control_schema['required']
177 control_schema['items']['required'] = required
178
179 ui_schemas.append({
180 'key': key,
181 'templateOptions': {
182 'objectTemplateOptions': {
183 'layoutType': self.layout_type()
184 }
185 },
186 'items': []
187 })
188 items = ui_schemas[-1]['items']
189 else:
190 control_schema['properties'] = {}
191 control_schema['required'] = []
192 required = control_schema['required']
193 properties = control_schema['properties']
194 ui_schemas.append({
195 'templateOptions': {
196 'layoutType': self.layout_type()
197 },
198 'key': key,
199 'items': []
200 })
201 if key:
202 items = ui_schemas[-1]['items']
203 else:
204 items = ui_schemas
205
206 assert items is not None
207 assert properties is not None
208 assert required is not None
209
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
215 if key:
216 if self._property_type() == 'array':
217 field_key = key + '[].' + field.key
218 else:
219 field_key = key + '.' + field.key
220
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)
236 return {
237 'ui_schema': ui_schemas,
238 'control_schema': control_schema,
239 }
240
241
242 class VerticalContainer(Container):
243 def layout_type(self):
244 return 'column'
245
246 def _property_type(self):
247 return 'object'
248
249
250 class HorizontalContainer(Container):
251 def layout_type(self):
252 return 'row'
253
254 def _property_type(self):
255 return 'object'
256
257
258 class ArrayVerticalContainer(Container):
259 def layout_type(self):
260 return 'column'
261
262 def _property_type(self):
263 return 'array'
264
265
266 class ArrayHorizontalContainer(Container):
267 def layout_type(self):
268 return 'row'
269
270 def _property_type(self):
271 return 'array'
272
273
274 class FormTaskInfo:
275 def __init__(self, message: str, metadata_fields: List[str]) -> None:
276 self.message = message
277 self.metadata_fields = metadata_fields
278
279 def to_dict(self):
280 return {'message': self.message, 'metadataFields': self.metadata_fields}
281
282
283 class Form:
284 def __init__(self, path, root_container, method_type='',
285 task_info: FormTaskInfo = FormTaskInfo("Unknown task", []),
286 model_callback=None):
287 self.path = path
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
292
293 def to_dict(self):
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
299 return res
300
301
302 class CRUDMeta(SerializableClass):
303 def __init__(self):
304 self.table = TableComponent()
305 self.permissions = []
306 self.actions = []
307 self.forms = []
308 self.columnKey = ''
309 self.detail_columns = []
310
311
312 class CRUDCollectionMethod(NamedTuple):
313 func: Callable[..., Iterable[Any]]
314 doc: EndpointDoc
315
316
317 class CRUDResourceMethod(NamedTuple):
318 func: Callable[..., Any]
319 doc: EndpointDoc
320
321
322 # pylint: disable=R0902
323 class CRUDEndpoint:
324 # for testing purposes
325 CRUDClass: Optional[RESTController] = None
326 CRUDClassMetadata: Optional[RESTController] = None
327
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):
340 self.router = router
341 self.doc = doc
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 []
345 self.meta = meta
346 self.get_all = get_all
347 self.create = create
348 self.delete = delete
349 self.edit = edit
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
355
356 def __call__(self, cls: Any):
357 self.create_crud_class(cls)
358
359 self.meta.table.columns.extend(TableColumn(prop=field) for field in cls._fields)
360 self.create_meta_class(cls)
361 return cls
362
363 def create_crud_class(self, cls):
364 outer_self: CRUDEndpoint = self
365
366 funcs = {}
367 if self.get_all:
368 @self.get_all.doc
369 @wraps(self.get_all.func)
370 def _list(self, *args, **kwargs):
371 items = []
372 for item in outer_self.get_all.func(self, *args, **kwargs): # type: ignore
373 items.append(serialize(cls(**item)))
374 return items
375 funcs['list'] = _list
376
377 if self.create:
378 @self.create.doc
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
383
384 if self.delete:
385 @self.delete.doc
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
390
391 if self.edit:
392 @self.edit.doc
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
397
398 for extra_endpoint in self.extra_endpoints:
399 funcs[extra_endpoint[0]] = extra_endpoint[1].doc(extra_endpoint[1].func)
400
401 class_name = self.router.path.replace('/', '')
402 crud_class = type(f'{class_name}_CRUDClass',
403 (RESTController,),
404 {
405 **funcs,
406 'outer_self': self,
407 })
408 self.router(self.doc(crud_class))
409 cls.CRUDClass = crud_class
410
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)
422
423 def get_detail_columns(self):
424 columns = self.__class__.outer_self.detail_columns
425 self.__class__.outer_self.meta.detail_columns = columns
426
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]
432 new_template = ""
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,
438 new_template,
439 hidden,
440 column.filterable,
441 flex_grow)
442 self.__class__.outer_self.meta.table.columns[i] = new_column
443
444 def generate_actions(self):
445 self.__class__.outer_self.meta.actions.clear()
446
447 for action in self.__class__.outer_self.actions:
448 self.__class__.outer_self.meta.actions.append(action._asdict())
449
450 def generate_forms(self, model_key):
451 self.__class__.outer_self.meta.forms.clear()
452
453 for form in self.__class__.outer_self.forms:
454 form_as_dict = form.to_dict()
455 model = {}
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)
460
461 def set_permissions(self):
462 self.__class__.outer_self.meta.permissions.clear()
463
464 if self.__class__.outer_self.permissions:
465 self.outer_self.meta.permissions.extend(self.__class__.outer_self.permissions)
466
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
470
471 class_name = self.router.path.replace('/', '')
472 meta_class = type(f'{class_name}_CRUDClassMetadata',
473 (RESTController,),
474 {
475 'list': _list,
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,
482 'outer_self': self,
483 })
484 UIRouter(self.router.path, self.router.security_scope)(meta_class)
485 cls.CRUDClassMetadata = meta_class