]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: replace ajsf with formly
authorPere Diaz Bou <pdiazbou@redhat.com>
Mon, 6 Mar 2023 19:32:24 +0000 (20:32 +0100)
committerPere Diaz Bou <pere-altea@hotmail.com>
Fri, 5 May 2023 16:43:11 +0000 (18:43 +0200)
ajsf json schema library for angular doesn't seem to be actively
maintained. Instead, fromly is a well maintained replacement with extra
stuff like validators builtin, support for json schemas, custom
components, etc...

Textareas weren't supported on ajsf, therefore, it made sense to move to
this dep instead.

Signed-off-by: Pere Diaz Bou <pdiazbou@redhat.com>
Signed-off-by: Nizamudeen A <nia@redhat.com>
(cherry picked from commit 2c43dd0c16e3cc3b3eada03ed11958a689cc4bcd)
(cherry picked from commit 768cfbcfe7d937fc34e8afde68974c04569ba962)

35 files changed:
src/pybind/mgr/dashboard/controllers/_crud.py
src/pybind/mgr/dashboard/controllers/ceph_users.py
src/pybind/mgr/dashboard/frontend/package-lock.json
src/pybind/mgr/dashboard/frontend/package.json
src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-form/crud-form.component.html [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-form/crud-form.component.scss [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-form/crud-form.component.spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-form/crud-form.component.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-table.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/datatable.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.model.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/services/crud-form-adapter.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/services/crud-form-adapter.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/services/data-gateway.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/shared.module.ts
src/pybind/mgr/dashboard/frontend/src/styles.scss
src/pybind/mgr/dashboard/tests/test_ceph_users.py
src/pybind/mgr/dashboard/tests/test_crud.py

index 0ac299db6e4674f6948e2976c348c74b0d349c7c..cbf87e54be3754b55eaa0a1137916db1dffb2432 100644 (file)
@@ -89,16 +89,13 @@ class FormField(NamedTuple):
     field_type: Any = str
     default_value: Optional[Any] = None
     optional: bool = False
-    html_class: str = ''
-    label_html_class: str = 'col-form-label'
-    field_html_class: str = 'col-form-input'
 
     def get_type(self):
         _type = ''
         if self.field_type == str:
             _type = 'string'
         elif self.field_type == int:
-            _type = 'integer'
+            _type = 'int'
         elif self.field_type == bool:
             _type = 'boolean'
         else:
@@ -108,15 +105,12 @@ class FormField(NamedTuple):
 
 class Container:
     def __init__(self, name: str, key: str, fields: List[Union[FormField, "Container"]],
-                 optional: bool = False, html_class: str = '', label_html_class: str = '',
-                 field_html_class: str = ''):
+                 optional: bool = False, min_items=1):
         self.name = name
         self.key = key
         self.fields = fields
         self.optional = optional
-        self.html_class = html_class
-        self.label_html_class = label_html_class
-        self.field_html_class = field_html_class
+        self.min_items = min_items
 
     def layout_type(self):
         raise NotImplementedError
@@ -135,39 +129,36 @@ class Container:
         properties = None  # control schema properties alias
         required = None
         if self._property_type() == 'array':
+            control_schema['required'] = []
+            control_schema['minItems'] = self.min_items
             control_schema['items'] = {
                 'type': 'object',
                 'properties': {},
                 'required': []
             }
             properties = control_schema['items']['properties']
-            required = control_schema['items']['required']
+            required = control_schema['required']
+            control_schema['items']['required'] = required
+
             ui_schemas.append({
-                'type': 'array',
                 'key': key,
-                'htmlClass': self.html_class,
-                'fieldHtmlClass': self.field_html_class,
-                'labelHtmlClass': self.label_html_class,
-                'items': [{
-                        'type': 'div',
-                        'flex-direction': self.layout_type(),
-                        'displayFlex': True,
-                        'items': []
-                }]
+                'templateOptions': {
+                    'objectTemplateOptions': {
+                        'layoutType': self.layout_type()
+                    }
+                },
+                'items': []
             })
-            items = ui_schemas[-1]['items'][0]['items']
+            items = ui_schemas[-1]['items']
         else:
             control_schema['properties'] = {}
             control_schema['required'] = []
             required = control_schema['required']
             properties = control_schema['properties']
             ui_schemas.append({
-                'type': 'section',
-                'flex-direction': self.layout_type(),
-                'displayFlex': True,
-                'htmlClass': self.html_class,
-                'fieldHtmlClass': self.field_html_class,
-                'labelHtmlClass': self.label_html_class,
+                'templateOptions': {
+                    'layoutType': self.layout_type()
+                },
                 'key': key,
                 'items': []
             })
@@ -196,13 +187,10 @@ class Container:
                 properties[field.key]['type'] = _type
                 properties[field.key]['title'] = field.name
                 field_ui_schema['key'] = field_key
-                field_ui_schema['htmlClass'] = field.html_class
-                field_ui_schema['fieldHtmlClass'] = field.field_html_class
-                field_ui_schema['labelHtmlClass'] = field.label_html_class
                 items.append(field_ui_schema)
             elif isinstance(field, Container):
                 container_schema = field.to_dict(key+'.'+field.key if key else field.key)
-                control_schema['properties'][field.key] = container_schema['control_schema']
+                properties[field.key] = container_schema['control_schema']
                 ui_schemas.extend(container_schema['ui_schema'])
             if not field.optional:
                 required.append(field.key)
@@ -245,44 +233,12 @@ class ArrayHorizontalContainer(Container):
 
 
 class Form:
-    def __init__(self, path, root_container, action: str = '',
-                 footer_html_class: str = 'card-footer position-absolute pb-0 mt-3',
-                 submit_style: str = 'btn btn-primary', cancel_style: str = ''):
+    def __init__(self, path, root_container):
         self.path = path
-        self.action = action
         self.root_container = root_container
-        self.footer_html_class = footer_html_class
-        self.submit_style = submit_style
-        self.cancel_style = cancel_style
 
     def to_dict(self):
-        container_schema = self.root_container.to_dict()
-
-        # root container style
-        container_schema['ui_schema'].append({
-            'type': 'flex',
-            'flex-flow': f'{self.root_container.layout_type()} wrap',
-            'displayFlex': True,
-        })
-
-        footer = {
-            "type": "flex",
-            "htmlClass": self.footer_html_class,
-            "items": [
-                {
-                    'type': 'flex',
-                    'flex-direction': 'row',
-                    'displayFlex': True,
-                    'htmlClass': 'd-flex justify-content-end mb-0',
-                    'items': [
-                        {"type": "cancel", "style": self.cancel_style, 'htmlClass': 'mr-2'},
-                        {"type": "submit", "style": self.submit_style, "title": self.action},
-                    ]
-                }
-            ]
-        }
-        container_schema['ui_schema'].append(footer)
-        return container_schema
+        return self.root_container.to_dict()
 
 
 class CRUDMeta(SerializableClass):
index 3562f33a227122c28d0c7f7158e0ec6f9ffd386a..cf49ee6f5186d92140eefa3530636c4ad70470e8 100644 (file)
@@ -57,23 +57,20 @@ class CephUserEndpoints:
             raise DashboardException(msg, code=500)
         return f"Successfully created user '{user_entity}'"
 
-
-create_cap_container = ArrayHorizontalContainer('Capabilities', 'capabilities',
-                                                label_html_class='hidden cd-header mt-1', fields=[
-                                                    FormField('Entity', 'entity',
-                                                              field_type=str, html_class='mr-3'),
-                                                    FormField('Entity Capabilities',
-                                                              'cap', field_type=str)
-                                                ])
-create_container = VerticalContainer('Create User', 'create_user',
-                                     html_class='d-none', fields=[
-                                         FormField('User entity', 'user_entity',
-                                                   field_type=str),
-                                         create_cap_container,
-                                     ])
+create_cap_container = ArrayHorizontalContainer('Capabilities', 'capabilities', fields=[
+    FormField('Entity', 'entity',
+              field_type=str),
+    FormField('Entity Capabilities',
+              'cap', field_type=str)
+], min_items=1)
+create_container = VerticalContainer('Create User', 'create_user', fields=[
+    FormField('User entity', 'user_entity',
+              field_type=str),
+    create_cap_container,
+])
 
 create_form = Form(path='/cluster/user/create',
-                   root_container=create_container, action='Create User')
+                   root_container=create_container)
 
 
 @CRUDEndpoint(
index 0546043000d7d4a9937e9e6e4e423fc83f721ac1..96ae1dc6c583e128bde94901bd76bf6ddf1a9721 100644 (file)
       "integrity": "sha512-20Pk2Z98fbPLkECcrZSJszKos/OgtvJJR3NcbVfgCJ6EQjDNzW2P1BKqImOz3tJ952dvO2DWEhcLhQ1Wz1e9ng==",
       "dev": true
     },
-    "@ajsf/bootstrap4": {
-      "version": "0.7.0",
-      "resolved": "https://registry.npmjs.org/@ajsf/bootstrap4/-/bootstrap4-0.7.0.tgz",
-      "integrity": "sha512-wn6wIQeWknmn/t96XZgihfFq/jjr9GkV9P5dHEU+i9wQbxPNL1MS+x4tLWj9LH3Mx5RiC0Dr4gPgbkDd/bzLxg==",
-      "requires": {
-        "@ajsf/core": "~0.7.0",
-        "lodash-es": "~4.17.21",
-        "tslib": "^2.0.0"
-      }
-    },
-    "@ajsf/core": {
-      "version": "0.7.0",
-      "resolved": "https://registry.npmjs.org/@ajsf/core/-/core-0.7.0.tgz",
-      "integrity": "sha512-mysKftZAxT0bHYoia7LzbSinK7Z55wINS63zeK/rqSs9r2dF01Vxtzlx2ITViiok3TQ0UV+1OYce/piozEf4aw==",
-      "requires": {
-        "ajv": "^6.10.0",
-        "lodash-es": "~4.17.21",
-        "tslib": "^2.0.0"
-      },
-      "dependencies": {
-        "ajv": {
-          "version": "6.12.6",
-          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
-          "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
-          "requires": {
-            "fast-deep-equal": "^3.1.1",
-            "fast-json-stable-stringify": "^2.0.0",
-            "json-schema-traverse": "^0.4.1",
-            "uri-js": "^4.2.2"
-          }
-        },
-        "json-schema-traverse": {
-          "version": "0.4.1",
-          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
-        }
-      }
-    },
     "@ampproject/remapping": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
       "integrity": "sha512-wmgOI5sogAuilwBZJqCHVMjm2uhDxjdSmNLFx7eznwGDa6LjvjuATqCv2dVlftq0Y/5oZFVrg5NpyHt5kfZ8Cg==",
       "dev": true
     },
+    "@ngx-formly/bootstrap": {
+      "version": "6.1.1",
+      "resolved": "https://registry.npmjs.org/@ngx-formly/bootstrap/-/bootstrap-6.1.1.tgz",
+      "integrity": "sha512-yNzASqUrzvhMndERzoTBCvj1rtsgsmMXiXsqIP7PRJ4AdGtsTZvpxNYZAltdKEgJvc1hS/lDMJdS7IHg2qFN9Q==",
+      "requires": {
+        "tslib": "^2.0.0"
+      }
+    },
+    "@ngx-formly/core": {
+      "version": "6.1.1",
+      "resolved": "https://registry.npmjs.org/@ngx-formly/core/-/core-6.1.1.tgz",
+      "integrity": "sha512-6Fg9TBcXXrnUkHqVlpCQbVE5BWJQBvCitQRngW7kiA/+86rhH5mkL19enULWKq7fEMi54uCVvWsz7l6VOaJhLA==",
+      "requires": {
+        "tslib": "^2.0.0"
+      }
+    },
     "@nodelib/fs.scandir": {
       "version": "2.1.5",
       "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
     "fast-deep-equal": {
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
-      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+      "dev": true
     },
     "fast-glob": {
       "version": "3.2.12",
     "fast-json-stable-stringify": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
-      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
+      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+      "dev": true
     },
     "fast-levenshtein": {
       "version": "2.0.6",
       "version": "4.4.1",
       "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
       "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+      "dev": true,
       "requires": {
         "punycode": "^2.1.0"
       },
index 6ca0fddfda7e27839ad3bccec91234b484106e41..8ff70bcf15cf5d28d5b51bc15ce26a611a50828d 100644 (file)
@@ -44,8 +44,6 @@
   },
   "private": true,
   "dependencies": {
-    "@ajsf/bootstrap4": "0.7.0",
-    "@ajsf/core": "0.7.0",
     "@angular/animations": "13.3.11",
     "@angular/common": "13.3.11",
     "@angular/compiler": "13.3.11",
@@ -57,6 +55,8 @@
     "@angular/router": "13.3.11",
     "@circlon/angular-tree-component": "10.0.0",
     "@ng-bootstrap/ng-bootstrap": "12.1.2",
+    "@ngx-formly/bootstrap": "6.1.1",
+    "@ngx-formly/core": "6.1.1",
     "@popperjs/core": "2.10.2",
     "@swimlane/ngx-datatable": "18.0.0",
     "@types/file-saver": "2.0.1",
index 6880a1561c1d9d281d264a41dfd286be8344d484..badaedd996a360f2358b9af327c99e549a30f00f 100644 (file)
@@ -37,7 +37,7 @@ import { LoginLayoutComponent } from './core/layouts/login-layout/login-layout.c
 import { WorkbenchLayoutComponent } from './core/layouts/workbench-layout/workbench-layout.component';
 import { ApiDocsComponent } from './core/navigation/api-docs/api-docs.component';
 import { ActionLabels, URLVerbs } from './shared/constants/app.constants';
-import { CrudFormComponent } from './shared/datatable/crud-table/crud-form/crud-form.component';
+import { CrudFormComponent } from './shared/forms/crud-form/crud-form.component';
 import { CRUDTableComponent } from './shared/datatable/crud-table/crud-table.component';
 import { BreadcrumbsResolver, IBreadcrumb } from './shared/models/breadcrumbs';
 import { AuthGuardService } from './shared/services/auth-guard.service';
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-form/crud-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-form/crud-form.component.html
deleted file mode 100644 (file)
index 8d7b21b..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-<div class="cd-col-form">
-  <div class="card pb-0">
-    <div i18n="form title"
-         class="card-header">{{ title }}</div>
-
-    <div class="card-body position-relative">
-      <json-schema-form
-          *ngIf="controlSchema && uiSchema"
-          [schema]="controlSchema"
-          [layout]="uiSchema"
-          [data]="data"
-          [widgets]="widgets"
-          (onSubmit)="submit($event)"
-          [options]="formOptions"
-          framework="bootstrap-4">
-      </json-schema-form>
-
-    </div>
-  </div>
-</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-form/crud-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-form/crud-form.component.scss
deleted file mode 100644 (file)
index 6d21e4c..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-@use './src/styles/vendor/variables' as vv;
-
-::ng-deep json-schema-form {
-  label.control-label.hidden {
-    display: none;
-  }
-
-  .form-group.schema-form-submit p {
-    display: none;
-  }
-
-  legend {
-    font-weight: 100 !important;
-  }
-
-  .card-footer {
-    border: 1px solid rgba(0, 0, 0, 0.125);
-    left: -1px;
-    width: -webkit-fill-available;
-    width: -moz-available;
-  }
-}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-form/crud-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-form/crud-form.component.spec.ts
deleted file mode 100644 (file)
index 7a6faa7..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { ToastrModule, ToastrService } from 'ngx-toastr';
-import { configureTestBed } from '~/testing/unit-test-helper';
-import { CdDatePipe } from '~/app/shared/pipes/cd-date.pipe';
-import { CrudFormComponent } from './crud-form.component';
-import { RouterTestingModule } from '@angular/router/testing';
-
-describe('CrudFormComponent', () => {
-  let component: CrudFormComponent;
-  let fixture: ComponentFixture<CrudFormComponent>;
-  const toastFakeService = {
-    error: () => true,
-    info: () => true,
-    success: () => true
-  };
-
-  configureTestBed({
-    imports: [ToastrModule.forRoot(), RouterTestingModule, HttpClientTestingModule],
-    providers: [
-      { provide: ToastrService, useValue: toastFakeService },
-      { provide: CdDatePipe, useValue: { transform: (d: any) => d } }
-    ]
-  });
-
-  beforeEach(async () => {
-    await TestBed.configureTestingModule({
-      declarations: [CrudFormComponent]
-    }).compileComponents();
-  });
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(CrudFormComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-form/crud-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/crud-table/crud-form/crud-form.component.ts
deleted file mode 100644 (file)
index 4545f2b..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-import { Component, OnInit } from '@angular/core';
-import { ActivatedRoute } from '@angular/router';
-import { DataGatewayService } from '~/app/shared/services/data-gateway.service';
-import { BackButtonComponent } from '~/app/shared/components/back-button/back-button.component';
-import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
-import { FinishedTask } from '~/app/shared/models/finished-task';
-import { Location } from '@angular/common';
-
-@Component({
-  selector: 'cd-crud-form',
-  templateUrl: './crud-form.component.html',
-  styleUrls: ['./crud-form.component.scss']
-})
-export class CrudFormComponent implements OnInit {
-  uiSchema: any;
-  controlSchema: any;
-  data: any;
-  widgets: any = {
-    cancel: BackButtonComponent
-  };
-  resource: string;
-  title: string;
-
-  formOptions = {
-    defautWidgetOptions: {
-      validationMessages: {
-        required: 'This field is required'
-      }
-    }
-  };
-  constructor(
-    private dataGatewayService: DataGatewayService,
-    private activatedRoute: ActivatedRoute,
-    private taskWrapper: TaskWrapperService,
-    private location: Location
-  ) {}
-
-  ngOnInit(): void {
-    this.activatedRoute.data.subscribe((data: any) => {
-      this.resource = data.resource;
-      this.dataGatewayService.list(`ui-${this.resource}`).subscribe((response: any) => {
-        this.title = response.forms[0].control_schema.title;
-        this.uiSchema = response.forms[0].ui_schema;
-        this.controlSchema = response.forms[0].control_schema;
-      });
-    });
-  }
-
-  submit(data: any) {
-    if (data) {
-      this.taskWrapper
-        .wrapTaskAroundCall({
-          task: new FinishedTask('ceph-user/create', {
-            user_entity: data.user_entity
-          }),
-          call: this.dataGatewayService.create(this.resource, data)
-        })
-        .subscribe({
-          complete: () => {
-            this.location.back();
-          }
-        });
-    }
-  }
-}
index b9b4ae62b34ddf84c508c59a5e1e2e422d98c8b8..d1aed4462d7f7ec949fd49ad33db3964ad7f2f84 100644 (file)
                         [tableActions]="meta.actions">
       </cd-table-actions>
     </div>
+    <ng-container *ngIf="expandedRow && meta.detail_columns.length > 0"
+                  cdTableDetail>
+      <table class="table table-striped table-bordered">
+        <tbody>
+          <tr *ngFor="let column of meta.detail_columns">
+            <td i18n
+                class="bold">{{ column }}</td>
+            <td> {{ expandedRow[column] }} </td>
+          </tr>
+        </tbody>
+      </table>
+    </ng-container>
 
   </cd-table>
 </ng-container>
index 1cef20b31e358e66d567e5733a53f1b4b12e9332..5f1a5f99175b436a81e4834f075ee337d23b80f1 100644 (file)
@@ -1,12 +1,14 @@
 import { CommonModule } from '@angular/common';
 import { NgModule } from '@angular/core';
-import { FormsModule } from '@angular/forms';
 import { RouterModule } from '@angular/router';
 
 import { NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
 import { NgxDatatableModule } from '@swimlane/ngx-datatable';
 import { NgxPipeFunctionModule } from 'ngx-pipe-function';
 
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { FormlyModule } from '@ngx-formly/core';
+import { FormlyBootstrapModule } from '@ngx-formly/bootstrap';
 import { ComponentsModule } from '../components/components.module';
 import { PipesModule } from '../pipes/pipes.module';
 import { CRUDTableComponent } from './crud-table/crud-table.component';
@@ -14,8 +16,10 @@ import { TableActionsComponent } from './table-actions/table-actions.component';
 import { TableKeyValueComponent } from './table-key-value/table-key-value.component';
 import { TablePaginationComponent } from './table-pagination/table-pagination.component';
 import { TableComponent } from './table/table.component';
-import { Bootstrap4FrameworkModule } from '@ajsf/bootstrap4';
-import { CrudFormComponent } from './crud-table/crud-form/crud-form.component';
+import { CrudFormComponent } from '../forms/crud-form/crud-form.component';
+import { FormlyArrayTypeComponent } from '../forms/crud-form/formly-array-type/formly-array-type.component';
+import { FormlyInputTypeComponent } from '../forms/crud-form/formly-input-type/formly-input-type.component';
+import { FormlyObjectTypeComponent } from '../forms/crud-form/formly-object-type/formly-object-type.component';
 
 @NgModule({
   imports: [
@@ -28,7 +32,16 @@ import { CrudFormComponent } from './crud-table/crud-form/crud-form.component';
     PipesModule,
     ComponentsModule,
     RouterModule,
-    Bootstrap4FrameworkModule
+    ReactiveFormsModule,
+    FormlyModule.forRoot({
+      types: [
+        { name: 'array', component: FormlyArrayTypeComponent },
+        { name: 'object', component: FormlyObjectTypeComponent },
+        { name: 'input', component: FormlyInputTypeComponent }
+      ],
+      validationMessages: [{ name: 'required', message: 'This field is required' }]
+    }),
+    FormlyBootstrapModule
   ],
   declarations: [
     TableComponent,
@@ -36,7 +49,10 @@ import { CrudFormComponent } from './crud-table/crud-form/crud-form.component';
     TableActionsComponent,
     CRUDTableComponent,
     TablePaginationComponent,
-    CrudFormComponent
+    CrudFormComponent,
+    FormlyArrayTypeComponent,
+    FormlyInputTypeComponent,
+    FormlyObjectTypeComponent
   ],
   exports: [
     TableComponent,
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.html
new file mode 100644 (file)
index 0000000..9102b89
--- /dev/null
@@ -0,0 +1,25 @@
+<div class="cd-col-form">
+  <div class="card pb-0"
+       *ngIf="formUISchema$ | async as formUISchema">
+    <div i18n="form title"
+         class="card-header">{{ formUISchema.title }}</div>
+    <form *ngIf="formUISchema.uiSchema"
+          [formGroup]="form"
+          (ngSubmit)="submit(model)">
+
+      <div class="card-body position-relative">
+        <formly-form [form]="form"
+                     [fields]="formUISchema.controlSchema"
+                     [model]="model"
+                     [options]="options"></formly-form>
+      </div>
+      <div class="card-footer">
+        <cd-form-button-panel (submitActionEvent)="submit(model)"
+                              [form]="formDir"
+                              [submitText]="formUISchema.title"
+                              [disabled]="!form.valid"
+                              wrappingClass="text-right"></cd-form-button-panel>
+      </div>
+    </form>
+  </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.scss
new file mode 100644 (file)
index 0000000..6d21e4c
--- /dev/null
@@ -0,0 +1,22 @@
+@use './src/styles/vendor/variables' as vv;
+
+::ng-deep json-schema-form {
+  label.control-label.hidden {
+    display: none;
+  }
+
+  .form-group.schema-form-submit p {
+    display: none;
+  }
+
+  legend {
+    font-weight: 100 !important;
+  }
+
+  .card-footer {
+    border: 1px solid rgba(0, 0, 0, 0.125);
+    left: -1px;
+    width: -webkit-fill-available;
+    width: -moz-available;
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/crud-form.component.spec.ts
new file mode 100644 (file)
index 0000000..7a6faa7
--- /dev/null
@@ -0,0 +1,42 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ToastrModule, ToastrService } from 'ngx-toastr';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { CdDatePipe } from '~/app/shared/pipes/cd-date.pipe';
+import { CrudFormComponent } from './crud-form.component';
+import { RouterTestingModule } from '@angular/router/testing';
+
+describe('CrudFormComponent', () => {
+  let component: CrudFormComponent;
+  let fixture: ComponentFixture<CrudFormComponent>;
+  const toastFakeService = {
+    error: () => true,
+    info: () => true,
+    success: () => true
+  };
+
+  configureTestBed({
+    imports: [ToastrModule.forRoot(), RouterTestingModule, HttpClientTestingModule],
+    providers: [
+      { provide: ToastrService, useValue: toastFakeService },
+      { provide: CdDatePipe, useValue: { transform: (d: any) => d } }
+    ]
+  });
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [CrudFormComponent]
+    }).compileComponents();
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(CrudFormComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
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
new file mode 100644 (file)
index 0000000..3734bc8
--- /dev/null
@@ -0,0 +1,57 @@
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute } 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 { Location } from '@angular/common';
+import { FormGroup } from '@angular/forms';
+import { FormlyFormOptions } from '@ngx-formly/core';
+import { mergeMap } from 'rxjs/operators';
+import { JsonFormUISchema } from './crud-form.model';
+import { Observable } from 'rxjs';
+
+@Component({
+  selector: 'cd-crud-form',
+  templateUrl: './crud-form.component.html',
+  styleUrls: ['./crud-form.component.scss']
+})
+export class CrudFormComponent implements OnInit {
+  model: any = {};
+  options: FormlyFormOptions = {};
+  resource: string;
+  form = new FormGroup({});
+  formUISchema$: Observable<JsonFormUISchema>;
+
+  constructor(
+    private dataGatewayService: DataGatewayService,
+    private activatedRoute: ActivatedRoute,
+    private taskWrapper: TaskWrapperService,
+    private location: Location
+  ) {}
+
+  ngOnInit(): void {
+    this.formUISchema$ = this.activatedRoute.data.pipe(
+      mergeMap((data: any) => {
+        this.resource = data.resource;
+        return this.dataGatewayService.form(`ui-${this.resource}`);
+      })
+    );
+  }
+
+  submit(data: any) {
+    if (data) {
+      this.taskWrapper
+        .wrapTaskAroundCall({
+          task: new FinishedTask('ceph-user/create', {
+            user_entity: data.user_entity
+          }),
+          call: this.dataGatewayService.create(this.resource, data)
+        })
+        .subscribe({
+          complete: () => {
+            this.location.back();
+          }
+        });
+    }
+  }
+}
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
new file mode 100644 (file)
index 0000000..01574ed
--- /dev/null
@@ -0,0 +1,7 @@
+import { FormlyFieldConfig } from '@ngx-formly/core';
+
+export interface JsonFormUISchema {
+  title: string;
+  controlSchema: FormlyFieldConfig[];
+  uiSchema: any;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.html
new file mode 100644 (file)
index 0000000..7108323
--- /dev/null
@@ -0,0 +1,42 @@
+<div class="mb-3">
+  <legend *ngIf="props.label"
+          class="cd-header mt-1"
+          i18n>{{ props.label }}</legend>
+  <p *ngIf="props.description"
+     i18n>{{ props.description }}</p>
+
+  <div *ngFor="let field of field.fieldGroup; let i = index"
+       class="d-flex">
+    <formly-field class="col"
+                  [field]="field"></formly-field>
+    <div class="action-btn">
+      <button class="btn btn-light ms-1"
+              type="button"
+              (click)="addWrapper()">
+        <i [ngClass]="icons.add"></i>
+      </button>
+      <button class="btn btn-light ms-1"
+              type="button"
+              (click)="remove(i)"
+              *ngIf="field.props.removable !== false">
+        <i [ngClass]="icons.trash"></i>
+      </button>
+    </div>
+  </div>
+  <div *ngIf="field.fieldGroup.length === 0"
+       class="text-right">
+    <button class="btn btn-light"
+            type="button"
+            (click)="addWrapper()"
+            i18n>
+      <i [ngClass]="icons.add"></i>
+      Add {{ props.label }}
+    </button>
+  </div>
+
+  <span class="invalid-feedback"
+        role="alert"
+        *ngIf="showError && formControl.errors">
+    <formly-validation-message [field]="field"></formly-validation-message>
+  </span>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.scss
new file mode 100644 (file)
index 0000000..37d7465
--- /dev/null
@@ -0,0 +1,3 @@
+.action-btn {
+  margin-top: 2.4rem;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.spec.ts
new file mode 100644 (file)
index 0000000..58a20e2
--- /dev/null
@@ -0,0 +1,46 @@
+import { Component } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormGroup } from '@angular/forms';
+import { FormlyFieldConfig, FormlyModule } from '@ngx-formly/core';
+
+import { FormlyArrayTypeComponent } from './formly-array-type.component';
+
+@Component({
+  template: ` <form [formGroup]="form">
+    <formly-form [model]="{}" [fields]="fields" [options]="{}" [form]="form"></formly-form>
+  </form>`
+})
+class MockFormComponent {
+  form = new FormGroup({});
+  fields: FormlyFieldConfig[] = [
+    {
+      wrappers: ['input'],
+      defaultValue: {}
+    }
+  ];
+}
+describe('FormlyArrayTypeComponent', () => {
+  let component: MockFormComponent;
+  let fixture: ComponentFixture<MockFormComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [FormlyArrayTypeComponent],
+      imports: [
+        FormlyModule.forRoot({
+          types: [{ name: 'array', component: FormlyArrayTypeComponent }]
+        })
+      ]
+    }).compileComponents();
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(MockFormComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-array-type/formly-array-type.component.ts
new file mode 100644 (file)
index 0000000..dcbac70
--- /dev/null
@@ -0,0 +1,34 @@
+/**  Copyright 2021 Formly. All Rights Reserved.
+    Use of this source code is governed by an MIT-style license that
+    can be found in the LICENSE file at https://github.com/ngx-formly/ngx-formly/blob/main/LICENSE */
+
+import { Component, OnInit } from '@angular/core';
+import { FieldArrayType } from '@ngx-formly/core';
+import { forEach } from 'lodash';
+import { Icons } from '~/app/shared/enum/icons.enum';
+
+@Component({
+  selector: 'cd-formly-array-type',
+  templateUrl: './formly-array-type.component.html',
+  styleUrls: ['./formly-array-type.component.scss']
+})
+export class FormlyArrayTypeComponent extends FieldArrayType implements OnInit {
+  icons = Icons;
+
+  ngOnInit(): void {
+    this.propagateTemplateOptions();
+  }
+
+  addWrapper() {
+    this.add();
+    this.propagateTemplateOptions();
+  }
+
+  propagateTemplateOptions() {
+    forEach(this.field.fieldGroup, (field) => {
+      if (field.type == 'object') {
+        field.props.templateOptions = this.props.templateOptions.objectTemplateOptions;
+      }
+    });
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.html
new file mode 100644 (file)
index 0000000..e090e7d
--- /dev/null
@@ -0,0 +1,3 @@
+<input [formControl]="formControl"
+       class="form-control cd-col-form-input"
+       [formlyAttributes]="field">
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.spec.ts
new file mode 100644 (file)
index 0000000..0a36356
--- /dev/null
@@ -0,0 +1,47 @@
+import { Component } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormGroup } from '@angular/forms';
+import { FormlyFieldConfig, FormlyModule } from '@ngx-formly/core';
+
+import { FormlyInputTypeComponent } from './formly-input-type.component';
+
+@Component({
+  template: ` <form [formGroup]="form">
+    <formly-form [model]="{}" [fields]="fields" [options]="{}" [form]="form"></formly-form>
+  </form>`
+})
+class MockFormComponent {
+  form = new FormGroup({});
+  fields: FormlyFieldConfig[] = [
+    {
+      wrappers: ['input'],
+      defaultValue: {}
+    }
+  ];
+}
+
+describe('FormlyInputTypeComponent', () => {
+  let component: MockFormComponent;
+  let fixture: ComponentFixture<MockFormComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [FormlyInputTypeComponent],
+      imports: [
+        FormlyModule.forRoot({
+          types: [{ name: 'input', component: FormlyInputTypeComponent }]
+        })
+      ]
+    }).compileComponents();
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(MockFormComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-input-type/formly-input-type.component.ts
new file mode 100644 (file)
index 0000000..d310017
--- /dev/null
@@ -0,0 +1,9 @@
+import { Component } from '@angular/core';
+import { FieldType, FieldTypeConfig } from '@ngx-formly/core';
+
+@Component({
+  selector: 'cd-formly-input-type',
+  templateUrl: './formly-input-type.component.html',
+  styleUrls: ['./formly-input-type.component.scss']
+})
+export class FormlyInputTypeComponent extends FieldType<FieldTypeConfig> {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.html
new file mode 100644 (file)
index 0000000..84ec2ab
--- /dev/null
@@ -0,0 +1,17 @@
+<div class="mb-3">
+  <legend *ngIf="props.label"
+          class="cd-col-form-label"
+          i18n>{{ props.label }}</legend>
+  <p *ngIf="props.description"
+     i18n>{{ props.description }}</p>
+  <div class="alert alert-danger"
+       role="alert"
+       *ngIf="showError && formControl.errors">
+    <formly-validation-message [field]="field"></formly-validation-message>
+  </div>
+  <div [ngClass]="inputClass">
+    <formly-field *ngFor="let f of field.fieldGroup"
+                  [field]="f"
+                  class="flex-grow-1"></formly-field>
+  </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.spec.ts
new file mode 100644 (file)
index 0000000..b6c4dff
--- /dev/null
@@ -0,0 +1,47 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { FormlyObjectTypeComponent } from './formly-object-type.component';
+import { FormlyFieldConfig, FormlyModule } from '@ngx-formly/core';
+import { Component } from '@angular/core';
+import { FormGroup } from '@angular/forms';
+
+@Component({
+  template: ` <form [formGroup]="form">
+    <formly-form [model]="{}" [fields]="fields" [options]="{}" [form]="form"></formly-form>
+  </form>`
+})
+class MockFormComponent {
+  form = new FormGroup({});
+  fields: FormlyFieldConfig[] = [
+    {
+      wrappers: ['object'],
+      defaultValue: {}
+    }
+  ];
+}
+
+describe('FormlyObjectTypeComponent', () => {
+  let fixture: ComponentFixture<MockFormComponent>;
+  let mockComponent: MockFormComponent;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [FormlyObjectTypeComponent],
+      imports: [
+        FormlyModule.forRoot({
+          types: [{ name: 'object', component: FormlyObjectTypeComponent }]
+        })
+      ]
+    }).compileComponents();
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(MockFormComponent);
+    mockComponent = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(mockComponent).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/crud-form/formly-object-type/formly-object-type.component.ts
new file mode 100644 (file)
index 0000000..3dd7412
--- /dev/null
@@ -0,0 +1,22 @@
+/**  Copyright 2021 Formly. All Rights Reserved.
+    Use of this source code is governed by an MIT-style license that
+    can be found in the LICENSE file at https://github.com/ngx-formly/ngx-formly/blob/main/LICENSE */
+
+import { Component } from '@angular/core';
+import { FieldType } from '@ngx-formly/core';
+
+@Component({
+  selector: 'cd-formly-object-type',
+  templateUrl: './formly-object-type.component.html',
+  styleUrls: ['./formly-object-type.component.scss']
+})
+export class FormlyObjectTypeComponent extends FieldType {
+  get inputClass(): string {
+    const layoutType = this.props.templateOptions?.layoutType;
+    const defaultFlexClasses = 'd-flex justify-content-center align-content-stretch gap-3';
+    if (layoutType == 'row') {
+      return defaultFlexClasses + ' flex-row';
+    }
+    return defaultFlexClasses + ' flex-column';
+  }
+}
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
new file mode 100644 (file)
index 0000000..7905a11
--- /dev/null
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { CrudFormAdapterService } from './crud-form-adapter.service';
+
+describe('CrudFormAdapterService', () => {
+  let service: CrudFormAdapterService;
+
+  beforeEach(() => {
+    TestBed.configureTestingModule({});
+    service = TestBed.inject(CrudFormAdapterService);
+  });
+
+  it('should be created', () => {
+    expect(service).toBeTruthy();
+  });
+});
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
new file mode 100644 (file)
index 0000000..f9cd35f
--- /dev/null
@@ -0,0 +1,26 @@
+import { Injectable } from '@angular/core';
+import { FormlyJsonschema } from '@ngx-formly/core/json-schema';
+import { JsonFormUISchema } from '../forms/crud-form/crud-form.model';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class CrudFormAdapterService {
+  constructor(private formlyJsonschema: FormlyJsonschema) {}
+
+  processJsonSchemaForm(response: any): JsonFormUISchema {
+    const title = response.forms[0].control_schema.title;
+    const uiSchema = response.forms[0].ui_schema;
+    const cSchema = response.forms[0].control_schema;
+    let controlSchema = this.formlyJsonschema.toFieldConfig(cSchema).fieldGroup;
+    for (let i = 0; i < controlSchema.length; i++) {
+      for (let j = 0; j < uiSchema.length; j++) {
+        if (controlSchema[i].key == uiSchema[j].key) {
+          controlSchema[i].className = uiSchema[j].className;
+          controlSchema[i].props.templateOptions = uiSchema[j].templateOptions;
+        }
+      }
+    }
+    return { title, uiSchema, controlSchema };
+  }
+}
index 6617fdb314afc7db839664c786ade2f3d505474c..f6db42818772a703df9c868bb730bfd951b4fa77 100644 (file)
@@ -2,6 +2,9 @@ import { HttpClient } from '@angular/common/http';
 import { Injectable } from '@angular/core';
 
 import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+import { JsonFormUISchema } from '../forms/crud-form/crud-form.model';
+import { CrudFormAdapterService } from './crud-form-adapter.service';
 
 @Injectable({
   providedIn: 'root'
@@ -9,14 +12,12 @@ import { Observable } from 'rxjs';
 export class DataGatewayService {
   cache: { [keys: string]: Observable<any> } = {};
 
-  constructor(private http: HttpClient) {}
+  constructor(private http: HttpClient, private crudFormAdapater: CrudFormAdapterService) {}
 
   list(dataPath: string): Observable<any> {
     const cacheable = this.getCacheable(dataPath, 'get');
     if (this.cache[cacheable] === undefined) {
-      const match = dataPath.match(/(?<url>[^@]+)(?:@(?<version>.+))?/);
-      const url = match.groups.url.split('.').join('/');
-      const version = match.groups.version || '1.0';
+      const { url, version } = this.getUrlAndVersion(dataPath);
 
       this.cache[cacheable] = this.http.get<any>(url, {
         headers: { Accept: `application/vnd.ceph.api.v${version}+json` }
@@ -27,16 +28,38 @@ export class DataGatewayService {
   }
 
   create(dataPath: string, data: any): Observable<any> {
-    const match = dataPath.match(/(?<url>[^@]+)(?:@(?<version>.+))?/);
-    const url = match.groups.url.split('.').join('/');
-    const version = match.groups.version || '1.0';
+    const { url, version } = this.getUrlAndVersion(dataPath);
 
     return this.http.post<any>(url, data, {
       headers: { Accept: `application/vnd.ceph.api.v${version}+json` }
     });
   }
 
+  form(dataPath: string): Observable<JsonFormUISchema> {
+    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, {
+        headers: { Accept: `application/vnd.ceph.api.v${version}+json` }
+      });
+    }
+    return this.cache[cacheable].pipe(
+      map((response) => {
+        return this.crudFormAdapater.processJsonSchemaForm(response);
+      })
+    );
+  }
+
   getCacheable(dataPath: string, method: string) {
     return dataPath + method;
   }
+
+  getUrlAndVersion(dataPath: string) {
+    const match = dataPath.match(/(?<url>[^@]+)(?:@(?<version>.+))?/);
+    const url = match.groups.url.split('.').join('/');
+    const version = match.groups.version || '1.0';
+
+    return { url: url, version: version };
+  }
 }
index ecc8bd03bfe6f6600f4e1ffc0f2caf1dd4212246..7756bed2d943af722aebd59de85e8d8bedd0f8dd 100644 (file)
@@ -1,6 +1,8 @@
 import { CommonModule } from '@angular/common';
 import { NgModule } from '@angular/core';
-import { Bootstrap4FrameworkModule } from '@ajsf/bootstrap4';
+import { ReactiveFormsModule } from '@angular/forms';
+import { FormlyModule } from '@ngx-formly/core';
+import { FormlyBootstrapModule } from '@ngx-formly/bootstrap';
 
 import { CssHelper } from '~/app/shared/classes/css-helper';
 import { ComponentsModule } from './components/components.module';
@@ -10,6 +12,9 @@ import { PipesModule } from './pipes/pipes.module';
 import { AuthGuardService } from './services/auth-guard.service';
 import { AuthStorageService } from './services/auth-storage.service';
 import { FormatterService } from './services/formatter.service';
+import { FormlyArrayTypeComponent } from './forms/crud-form/formly-array-type/formly-array-type.component';
+import { FormlyObjectTypeComponent } from './forms/crud-form/formly-object-type/formly-object-type.component';
+import { FormlyInputTypeComponent } from './forms/crud-form/formly-input-type/formly-input-type.component';
 
 @NgModule({
   imports: [
@@ -18,7 +23,17 @@ import { FormatterService } from './services/formatter.service';
     ComponentsModule,
     DataTableModule,
     DirectivesModule,
-    Bootstrap4FrameworkModule
+
+    ReactiveFormsModule,
+    FormlyModule.forRoot({
+      types: [
+        { name: 'array', component: FormlyArrayTypeComponent },
+        { name: 'object', component: FormlyObjectTypeComponent },
+        { name: 'input', component: FormlyInputTypeComponent }
+      ],
+      validationMessages: [{ name: 'required', message: 'This field is required' }]
+    }),
+    FormlyBootstrapModule
   ],
   declarations: [],
   exports: [ComponentsModule, PipesModule, DataTableModule, DirectivesModule],
index f2c3052f800abc681b903ad51fb8abada8c05f4a..4fdcb1fe3780bdf594869798244dffd5d56eccb2 100644 (file)
@@ -218,11 +218,7 @@ a.btn-light {
   @extend .badge, .bg-dark;
 }
 
-json-schema-form {
-  .help-block {
-    @extend .invalid-feedback;
-  }
-
+formly-form {
   .ng-touched.ng-invalid {
     @extend .is-invalid;
   }
@@ -230,4 +226,14 @@ json-schema-form {
   .ng-touched.ng-valid {
     @extend .is-valid;
   }
+
+  .form-label {
+    @extend .cd-col-form-label;
+    text-align: start;
+    width: 50%;
+
+    span[aria-hidden='true'] {
+      color: $danger;
+    }
+  }
 }
index 35029b32c653a525582c8ee5eb3f6f69dc7aa42b..9e0ee525b5ca0e73ef4b91cefc735fd2c441134d 100644 (file)
@@ -47,4 +47,6 @@ class CephUsersControllerTestCase(ControllerTestCase):
     def test_create_form(self):
         form_dict = create_form.to_dict()
         schema = {'schema': form_dict['control_schema'], 'layout': form_dict['ui_schema']}
-        validate(instance={'user_entity': 'foo', 'capabilities': []}, schema=schema['schema'])
+        validate(instance={'user_entity': 'foo',
+                           'capabilities': [{"entity": "mgr", "cap": "allow *"}]},
+                 schema=schema['schema'])
index 97c5728b388b40fba70e42ad86bab508fe558f82..a94dfad6254f9b6c98cd9bfc14581a143aff41f6 100644 (file)
@@ -43,20 +43,19 @@ def test_schema():
                 root_container=VerticalContainer('Create user', key='create_user', fields=[
                     FormField('User entity', key='user_entity', field_type=str),
                     ArrayHorizontalContainer('Capabilities', key='caps', fields=[
-                        FormField('left', field_type=str, key='left',
-                                  html_class='cd-col-form-input'),
+                        FormField('left', field_type=str, key='left'),
                         FormField('right', key='right', field_type=str)
                     ]),
                     ArrayVerticalContainer('ah', key='ah', fields=[
-                        FormField('top', key='top', field_type=str, label_html_class='d-none'),
+                        FormField('top', key='top', field_type=str),
                         FormField('bottom', key='bottom', field_type=str)
                     ]),
                     HorizontalContainer('oh', key='oh', fields=[
-                        FormField('left', key='left', field_type=str, label_html_class='d-none'),
+                        FormField('left', key='left', field_type=str),
                         FormField('right', key='right', field_type=str)
                     ]),
                     VerticalContainer('ov', key='ov', fields=[
-                        FormField('top', key='top', field_type=str, label_html_class='d-none'),
+                        FormField('top', key='top', field_type=str),
                         FormField('bottom', key='bottom', field_type=bool)
                     ]),
                 ]))